Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
cc409a9288
  1. 24
      src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx
  2. 10
      src/components/EventPowLabel/index.tsx
  3. 7
      src/components/KindFilter/index.tsx
  4. 5
      src/components/LiveActivitiesStrip.tsx
  5. 47
      src/components/NormalFeed/index.tsx
  6. 4
      src/components/Note/index.tsx
  7. 26
      src/components/NoteList/index.tsx
  8. 2
      src/components/RefreshButton/index.tsx
  9. 2
      src/components/ReplyNote/index.tsx
  10. 21
      src/components/Tabs/index.tsx
  11. 6
      src/components/ZapDialog/PostPaymentMessagePrompt.tsx
  12. 20
      src/components/ZapDialog/PublicMessageForm.tsx
  13. 22
      src/components/ZapDialog/SuperchatRequestForm.tsx
  14. 7
      src/constants.ts
  15. 3
      src/lib/console-log-buffer.ts
  16. 31
      src/lib/error-suppression.ts
  17. 12
      src/services/client-events.service.ts

24
src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx

@ -705,19 +705,19 @@ export default function AdvancedEventLabDialog({
minHeight: '12rem', minHeight: '12rem',
fontFamily: 'var(--font-mono, ui-monospace, monospace)' fontFamily: 'var(--font-mono, ui-monospace, monospace)'
}, },
// LanguageTool hits: drop default thin SVG underline, use thick wavy line (see `LT_GRAMMAR_MARK_CLASS`). // LanguageTool hits: subtle wavy underline (default CM lint SVG is hidden).
[`.cm-lintRange.${LT_GRAMMAR_MARK_CLASS}`]: { [`.cm-lintRange.${LT_GRAMMAR_MARK_CLASS}`]: {
backgroundImage: 'none', backgroundImage: 'none',
paddingBottom: '2px', paddingBottom: '1px',
textDecoration: 'underline', textDecoration: 'underline',
textDecorationSkipInk: 'none', textDecorationSkipInk: 'none',
textDecorationStyle: 'wavy', textDecorationStyle: 'wavy',
textDecorationColor: '#ea580c', textDecorationColor: 'rgba(234, 88, 12, 0.45)',
textDecorationThickness: '3px', textDecorationThickness: '1.5px',
textUnderlineOffset: '3px' textUnderlineOffset: '2px'
}, },
[`.cm-lintRange-active.${LT_GRAMMAR_MARK_CLASS}`]: { [`.cm-lintRange-active.${LT_GRAMMAR_MARK_CLASS}`]: {
backgroundColor: 'rgba(234, 88, 12, 0.22)' backgroundColor: 'rgba(234, 88, 12, 0.1)'
} }
}), }),
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
@ -742,16 +742,16 @@ export default function AdvancedEventLabDialog({
EditorView.theme({ EditorView.theme({
[`.cm-lintRange.${LT_GRAMMAR_MARK_CLASS}`]: { [`.cm-lintRange.${LT_GRAMMAR_MARK_CLASS}`]: {
backgroundImage: 'none', backgroundImage: 'none',
paddingBottom: '2px', paddingBottom: '1px',
textDecoration: 'underline', textDecoration: 'underline',
textDecorationSkipInk: 'none', textDecorationSkipInk: 'none',
textDecorationStyle: 'wavy', textDecorationStyle: 'wavy',
textDecorationColor: '#fdba74', textDecorationColor: 'rgba(251, 146, 60, 0.5)',
textDecorationThickness: '3px', textDecorationThickness: '1.5px',
textUnderlineOffset: '3px' textUnderlineOffset: '2px'
}, },
[`.cm-lintRange-active.${LT_GRAMMAR_MARK_CLASS}`]: { [`.cm-lintRange-active.${LT_GRAMMAR_MARK_CLASS}`]: {
backgroundColor: 'rgba(251, 146, 60, 0.28)' backgroundColor: 'rgba(251, 146, 60, 0.12)'
} }
}) })
) )
@ -988,7 +988,7 @@ export default function AdvancedEventLabDialog({
<AdvancedEventLabMarkupToolbar markupMode={markupMode} viewRef={markupView} sliceRef={sliceRef} /> <AdvancedEventLabMarkupToolbar markupMode={markupMode} viewRef={markupView} sliceRef={sliceRef} />
<div <div
ref={markupHost} ref={markupHost}
className="min-h-[24rem] h-[min(84vh,56rem)] overflow-hidden rounded-md border bg-muted/20" className="min-h-[16rem] h-[min(56vh,37.5rem)] overflow-hidden rounded-md border bg-muted/20"
/> />
</TabsContent> </TabsContent>

10
src/components/EventPowLabel/index.tsx

@ -20,16 +20,14 @@ export default function EventPowLabel({
return ( return (
<span <span
className={cn( className={cn(
'inline-flex shrink-0 items-center gap-1 rounded-md border-2 border-amber-500/90', 'inline-flex shrink-0 items-center gap-0.5 rounded border border-amber-500/20',
'bg-gradient-to-r from-amber-400/40 to-yellow-300/30 px-2 py-0.5', 'bg-amber-500/[0.08] px-1 py-px text-[10px] font-medium leading-none text-amber-800/75',
'text-xs font-bold uppercase tracking-wide text-amber-950 shadow-sm', 'dark:border-amber-400/15 dark:bg-amber-500/10 dark:text-amber-200/65',
'ring-2 ring-amber-400/35 dark:border-amber-400/80 dark:from-amber-500/30 dark:to-yellow-500/20',
'dark:text-amber-50 dark:ring-amber-300/25',
className className
)} )}
title={t('Proof of Work')} title={t('Proof of Work')}
> >
<Pickaxe className="size-3.5 shrink-0" strokeWidth={2.5} aria-hidden /> <Pickaxe className="size-2.5 shrink-0 opacity-60" strokeWidth={2} aria-hidden />
{t('POW {{difficulty}}', { difficulty })} {t('POW {{difficulty}}', { difficulty })}
</span> </span>
) )

7
src/components/KindFilter/index.tsx

@ -166,8 +166,9 @@ export default function KindFilter({
<Button <Button
variant="ghost" variant="ghost"
size="titlebar-icon" size="titlebar-icon"
aria-label={t('Filter')}
className={cn( className={cn(
'relative w-fit px-2 h-8 text-xs focus:text-foreground', 'relative h-8 w-fit shrink-0 px-1.5 text-xs focus:text-foreground',
!isDifferentFromSaved && !feedKindFilterBypass && 'text-muted-foreground', !isDifferentFromSaved && !feedKindFilterBypass && 'text-muted-foreground',
feedKindFilterBypass && 'text-amber-600 dark:text-amber-400' feedKindFilterBypass && 'text-amber-600 dark:text-amber-400'
)} )}
@ -177,8 +178,8 @@ export default function KindFilter({
} }
}} }}
> >
<ListFilter className="size-2.5" /> <ListFilter className="size-3.5 shrink-0" />
<span className="ml-1 text-xs">{t('Filter')}</span> <span className="ml-1 hidden min-[22rem]:inline">{t('Filter')}</span>
{isDifferentFromSaved && ( {isDifferentFromSaved && (
<div className="absolute size-1.5 rounded-full bg-primary left-6 top-1.5 ring-1 ring-background" /> <div className="absolute size-1.5 rounded-full bg-primary left-6 top-1.5 ring-1 ring-background" />
)} )}

5
src/components/LiveActivitiesStrip.tsx

@ -65,6 +65,9 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
const onSwipePointerDown = useCallback( const onSwipePointerDown = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => { (e: React.PointerEvent<HTMLDivElement>) => {
if (placement !== 'mobile' || items.length <= 1) return if (placement !== 'mobile' || items.length <= 1) return
// Taps on the note button / external link must not capture — capture retargets pointerup
// away from the button and suppresses its click (note never opens in the secondary panel).
if ((e.target as HTMLElement).closest('button, a, [role="tab"]')) return
swipeGrabRef.current = { x: e.clientX, y: e.clientY, pointerId: e.pointerId } swipeGrabRef.current = { x: e.clientX, y: e.clientY, pointerId: e.pointerId }
e.currentTarget.setPointerCapture(e.pointerId) e.currentTarget.setPointerCapture(e.pointerId)
}, },
@ -157,6 +160,7 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
> >
<button <button
type="button" type="button"
onPointerDown={(e) => e.stopPropagation()}
onClick={openLiveNote} onClick={openLiveNote}
className={cn( className={cn(
'flex min-w-0 flex-1 gap-2 rounded-md text-left transition-colors hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', 'flex min-w-0 flex-1 gap-2 rounded-md text-left transition-colors hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
@ -195,6 +199,7 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
)} )}
title={t('liveActivities.openJoinPageTitle')} title={t('liveActivities.openJoinPageTitle')}
aria-label={t('liveActivities.openJoinPageTitle')} aria-label={t('liveActivities.openJoinPageTitle')}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<ExternalLink className="size-4 shrink-0" aria-hidden /> <ExternalLink className="size-4 shrink-0" aria-hidden />

47
src/components/NormalFeed/index.tsx

@ -4,7 +4,7 @@ import { RefreshButton } from '@/components/RefreshButton'
import Tabs, { TabDefinition } from '@/components/Tabs' import Tabs, { TabDefinition } from '@/components/Tabs'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults' import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider' import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import { PROFILE_MEDIA_TAB_KINDS, FAST_READ_RELAY_URLS } from '@/constants' import { HOME_GALLERY_TAB_KINDS, FAST_READ_RELAY_URLS } from '@/constants'
import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import type { TPrimaryPageName } from '@/PageManager' import type { TPrimaryPageName } from '@/PageManager'
import { TFeedSubRequest, TNoteListMode } from '@/types' import { TFeedSubRequest, TNoteListMode } from '@/types'
@ -23,7 +23,7 @@ import {
import KindFilter from '../KindFilter' import KindFilter from '../KindFilter'
/** /**
* Home Gallery: favorites (or chip relays) first, then {@link FAST_READ_RELAY_URLS} so NIP-71 / picture / voice * Home Gallery: favorites (or chip relays) first, then {@link FAST_READ_RELAY_URLS} so NIP-71 / picture
* events are not starved when the users relay set is mostly text timelines. Deduped by normalized URL. * events are not starved when the users relay set is mostly text timelines. Deduped by normalized URL.
*/ */
function galleryRelayUrlsMergedWithReadLayer( function galleryRelayUrlsMergedWithReadLayer(
@ -193,7 +193,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
setFeedFilterTabRowHost((prev) => (Object.is(prev, node) ? prev : node)) setFeedFilterTabRowHost((prev) => (Object.is(prev, node) ? prev : node))
}, []) }, [])
const MEDIA_KINDS = useMemo(() => [...PROFILE_MEDIA_TAB_KINDS], []) const MEDIA_KINDS = useMemo(() => [...HOME_GALLERY_TAB_KINDS], [])
/** Every shard URL is a nostrarchives Wisp “trending notes” stream — replies/gallery tabs are not applicable. */ /** Every shard URL is a nostrarchives Wisp “trending notes” stream — replies/gallery tabs are not applicable. */
const isWispTrendingOnlyFeed = useMemo( const isWispTrendingOnlyFeed = useMemo(
@ -295,17 +295,25 @@ const NormalFeed = forwardRef<TNoteListRef, {
/** Include kind picker deps for single-relay chips (kindless REQ + client-side kinds). */ /** Include kind picker deps for single-relay chips (kindless REQ + client-side kinds). */
const subHeaderFilterDepsKey = `${allowKindlessRelayExplore ? 'kle' : 'std'}|${showKindsKey}|${feedKindFilterBypass}|${showAllKindsProp ? 'allProp' : 'k'}` const subHeaderFilterDepsKey = `${allowKindlessRelayExplore ? 'kle' : 'std'}|${showKindsKey}|${feedKindFilterBypass}|${showAllKindsProp ? 'allProp' : 'k'}`
const renderTabsInFeed = !(isMainFeed && setSubHeader) && !allowKindlessRelayExplore
const mergeFilterWithTabsRow =
showFeedClientFilter && ((isMainFeed && !!setSubHeader) || renderTabsInFeed)
/** Notes / Replies / Gallery switch, plus refresh + kind filter — on Wisp trending only the tool row (no mode tabs). */ /** Notes / Replies / Gallery switch, plus refresh + kind filter — on Wisp trending only the tool row (no mode tabs). */
const tabsElement = useMemo(() => { const tabsElement = useMemo(() => {
const kindRowOptions = ( const kindRowOptions = (
<div className="flex items-center gap-1"> <div className="flex items-center gap-0">
{onSubHeaderRefresh != null && <RefreshButton onClick={onSubHeaderRefresh} />} {onSubHeaderRefresh != null && <RefreshButton onClick={onSubHeaderRefresh} />}
<KindFilter showKinds={showKinds} onShowKindsChange={handleShowKindsChange} /> <KindFilter showKinds={showKinds} onShowKindsChange={handleShowKindsChange} />
{mergeFilterWithTabsRow ? (
<div ref={onFeedFilterTabRowSlotRef} className="flex items-center" />
) : null}
</div> </div>
) )
if (isMainFeed && isWispTrendingOnlyFeed) { if (isMainFeed && isWispTrendingOnlyFeed) {
return ( return (
<div className="flex w-full min-w-0 items-center justify-end gap-1 py-1">{kindRowOptions}</div> <div className="flex w-full min-w-0 items-center justify-end gap-0 py-1">{kindRowOptions}</div>
) )
} }
return ( return (
@ -324,13 +332,12 @@ const NormalFeed = forwardRef<TNoteListRef, {
handleListModeChange, handleListModeChange,
showKinds, showKinds,
onSubHeaderRefresh, onSubHeaderRefresh,
handleShowKindsChange handleShowKindsChange,
mergeFilterWithTabsRow
]) ])
const renderTabsInFeed = !(isMainFeed && setSubHeader) && !allowKindlessRelayExplore const tabRowChromeClass =
'w-full min-w-0 border-b border-border/80 bg-background/95 pb-1.5 pt-0.5 backdrop-blur supports-[backdrop-filter]:bg-background/80'
const mergeFilterWithTabsRow =
showFeedClientFilter && ((isMainFeed && !!setSubHeader) || renderTabsInFeed)
/** /**
* Push the tab row into {@link PrimaryPageLayout} subHeader. Use `useEffect` (not `useLayoutEffect`) so * Push the tab row into {@link PrimaryPageLayout} subHeader. Use `useEffect` (not `useLayoutEffect`) so
@ -345,15 +352,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
useEffect(() => { useEffect(() => {
if (!isMainFeed || !setSubHeader) return if (!isMainFeed || !setSubHeader) return
if (mergeFilterWithTabsRow) { if (mergeFilterWithTabsRow) {
setSubHeader( setSubHeader(<div className={tabRowChromeClass}>{tabsElement}</div>)
<div className="flex w-full min-w-0 flex-wrap items-end gap-x-2 gap-y-1 border-b border-border/80 bg-background/95 pb-1.5 pt-0.5 backdrop-blur supports-[backdrop-filter]:bg-background/80">
<div className="min-w-0 flex-1">{tabsElement}</div>
<div
ref={onFeedFilterTabRowSlotRef}
className="flex shrink-0 flex-col items-end self-start"
/>
</div>
)
} else { } else {
setSubHeader(tabsElement) setSubHeader(tabsElement)
} }
@ -376,15 +375,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
<> <>
{renderTabsInFeed && {renderTabsInFeed &&
(mergeFilterWithTabsRow ? ( (mergeFilterWithTabsRow ? (
<div className="sticky top-0 z-20 border-b border-border/80 bg-background/95 pb-1.5 pt-0.5 backdrop-blur supports-[backdrop-filter]:bg-background/80"> <div className={cn('sticky top-0 z-20', tabRowChromeClass)}>{tabsElement}</div>
<div className="flex w-full min-w-0 flex-wrap items-end gap-x-2 gap-y-1">
<div className="min-w-0 flex-1">{tabsElement}</div>
<div
ref={onFeedFilterTabRowSlotRef}
className="flex shrink-0 flex-col items-end self-start"
/>
</div>
</div>
) : ( ) : (
tabsElement tabsElement
))} ))}

4
src/components/Note/index.tsx

@ -693,6 +693,7 @@ export default function Note({
className="shrink-0 text-sm text-muted-foreground" className="shrink-0 text-sm text-muted-foreground"
short={isSmallScreen} short={isSmallScreen}
/> />
<EventPowLabel event={event} />
</div> </div>
) : isSyntheticRssParent ? ( ) : isSyntheticRssParent ? (
<> <>
@ -746,6 +747,7 @@ export default function Note({
className="shrink-0" className="shrink-0"
short={isSmallScreen} short={isSmallScreen}
/> />
<EventPowLabel event={event} />
</div> </div>
</div> </div>
) : ( ) : (
@ -763,6 +765,7 @@ export default function Note({
className="shrink-0" className="shrink-0"
short={isSmallScreen} short={isSmallScreen}
/> />
<EventPowLabel event={event} />
</span> </span>
</div> </div>
)} )}
@ -812,7 +815,6 @@ export default function Note({
)} )}
</div> </div>
</div> </div>
<EventPowLabel event={event} className="mt-1" />
{webReactionParentUrl ? ( {webReactionParentUrl ? (
<div className="mt-2 not-prose max-w-full" data-parent-note-preview> <div className="mt-2 not-prose max-w-full" data-parent-note-preview>
<WebPreview url={webReactionParentUrl} className="w-full" /> <WebPreview url={webReactionParentUrl} className="w-full" />

26
src/components/NoteList/index.tsx

@ -4,7 +4,8 @@ import {
ExtendedKind, ExtendedKind,
FAST_READ_RELAY_URLS, FAST_READ_RELAY_URLS,
FIRST_RELAY_RESULT_GRACE_MS, FIRST_RELAY_RESULT_GRACE_MS,
PROFILE_MEDIA_TAB_KINDS, HOME_GALLERY_TAB_KINDS,
HOME_GALLERY_TAB_KIND_SET,
SINGLE_RELAY_KINDLESS_EOSE_TIMEOUT_MS, SINGLE_RELAY_KINDLESS_EOSE_TIMEOUT_MS,
SINGLE_RELAY_KINDLESS_REQ_LIMIT SINGLE_RELAY_KINDLESS_REQ_LIMIT
} from '@/constants' } from '@/constants'
@ -1369,6 +1370,10 @@ const NoteList = forwardRef(
if (extraShouldHideEvent?.(evt)) return true if (extraShouldHideEvent?.(evt)) return true
if (homeFeedListMode === 'media' && !HOME_GALLERY_TAB_KIND_SET.has(evt.kind)) {
return true
}
if ( if (
homeFeedActiveSeenOnAllowlist && homeFeedActiveSeenOnAllowlist &&
homeFeedListMode === 'posts' && homeFeedListMode === 'posts' &&
@ -2607,7 +2612,7 @@ const NoteList = forwardRef(
} }
try { try {
const hits = client.eventService.listSessionEventsByKinds([...PROFILE_MEDIA_TAB_KINDS], { const hits = client.eventService.listSessionEventsByKinds([...HOME_GALLERY_TAB_KINDS], {
limit: 800 limit: 800
}) })
mergeLayer(hits as Event[], 'gallery_session_local') mergeLayer(hits as Event[], 'gallery_session_local')
@ -2619,7 +2624,7 @@ const NoteList = forwardRef(
try { try {
const since = dayjs().subtract(120, 'day').unix() const since = dayjs().subtract(120, 'day').unix()
const rows = await indexedDb.scanEventArchiveByKinds({ const rows = await indexedDb.scanEventArchiveByKinds({
kinds: [...PROFILE_MEDIA_TAB_KINDS], kinds: [...HOME_GALLERY_TAB_KINDS],
since, since,
maxRowsScanned: 28_000, maxRowsScanned: 28_000,
maxMatches: 220 maxMatches: 220
@ -3111,7 +3116,7 @@ const NoteList = forwardRef(
...(runtimeSnapshot.rawCount === 0 ...(runtimeSnapshot.rawCount === 0
? { ? {
emptyHint: emptyHint:
'All sub-batches returned 0 events: relays may not index these kinds for this author, the query may have timed out before slow relays EOSEd, or posts are kind 1 with links (this tab uses native media kinds only: picture, NIP-71 video regular/addressable, voice).' 'All sub-batches returned 0 events: relays may not index these kinds for this author, the query may have timed out before slow relays EOSEd, or posts are kind 1 with links (Gallery uses kinds 20, 21, 22, 34235 only).'
} }
: {}) : {})
}) })
@ -4485,7 +4490,7 @@ const NoteList = forwardRef(
useFeedFilterTabRowPortal && feedClientFilterTabRowHost useFeedFilterTabRowPortal && feedClientFilterTabRowHost
const feedClientFilterPanelSurfaceClass = feedClientFilterPanelPortalMode const feedClientFilterPanelSurfaceClass = feedClientFilterPanelPortalMode
? 'absolute top-full right-0 z-50 mt-1 w-[min(100vw-1rem,28rem)] max-w-[calc(100vw-1rem)] space-y-3 rounded-lg border border-border bg-background p-3 shadow-lg' ? 'space-y-3 border-b border-border/80 bg-background/95 px-2 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'
: 'space-y-3 border-t border-border/60 px-2 py-3' : 'space-y-3 border-t border-border/60 px-2 py-3'
const feedClientFilterSectionClass = 'space-y-2 rounded-md border border-border/60 bg-muted/25 p-2.5' const feedClientFilterSectionClass = 'space-y-2 rounded-md border border-border/60 bg-muted/25 p-2.5'
@ -4685,10 +4690,7 @@ const NoteList = forwardRef(
) : null ) : null
const feedClientFilterChrome = feedClientFilterPanelPortalMode ? ( const feedClientFilterChrome = feedClientFilterPanelPortalMode ? (
<div className="relative flex items-center gap-1"> feedClientFilterToggleButton
{feedClientFilterToggleButton}
{feedClientFilterPanel}
</div>
) : ( ) : (
<> <>
<div className="flex items-center gap-1">{feedClientFilterToggleButton}</div> <div className="flex items-center gap-1">{feedClientFilterToggleButton}</div>
@ -4696,6 +4698,10 @@ const NoteList = forwardRef(
</> </>
) )
/** Tab-row portal: toggle lives in the header; panel expands in-flow above the list. */
const feedClientFilterPanelInList =
feedClientFilterPanelPortalMode ? feedClientFilterPanel : null
const feedClientFilterBarEmbedded = ( const feedClientFilterBarEmbedded = (
<div className="sticky top-0 z-20 border-b border-border/80 bg-background/95 px-1 py-1 backdrop-blur supports-[backdrop-filter]:bg-background/80"> <div className="sticky top-0 z-20 border-b border-border/80 bg-background/95 px-1 py-1 backdrop-blur supports-[backdrop-filter]:bg-background/80">
{feedClientFilterChrome} {feedClientFilterChrome}
@ -4892,6 +4898,7 @@ const NoteList = forwardRef(
</div> </div>
) : null} ) : null}
{showFeedClientFilter ? feedClientFilterBar : null} {showFeedClientFilter ? feedClientFilterBar : null}
{feedClientFilterPanelInList}
{list} {list}
</div> </div>
</PullToRefresh> </PullToRefresh>
@ -4906,6 +4913,7 @@ const NoteList = forwardRef(
</div> </div>
) : null} ) : null}
{showFeedClientFilter ? feedClientFilterBar : null} {showFeedClientFilter ? feedClientFilterBar : null}
{feedClientFilterPanelInList}
{list} {list}
</div> </div>
)} )}

2
src/components/RefreshButton/index.tsx

@ -46,7 +46,7 @@ export function RefreshButton({
onClick() onClick()
setTimeout(() => setRefreshing(false), 500) setTimeout(() => setRefreshing(false), 500)
}} }}
className="text-muted-foreground focus:text-foreground [&_svg]:size-3 h-8 px-2 text-xs" className="h-8 shrink-0 px-1.5 text-muted-foreground focus:text-foreground [&_svg]:size-3"
> >
{refreshing ? ( {refreshing ? (
<Skeleton className="size-3 shrink-0 rounded-sm" aria-hidden /> <Skeleton className="size-3 shrink-0 rounded-sm" aria-hidden />

2
src/components/ReplyNote/index.tsx

@ -142,12 +142,12 @@ export default function ReplyNote({
className="shrink-0" className="shrink-0"
short={isSmallScreen} short={isSmallScreen}
/> />
<EventPowLabel event={event} />
</div> </div>
</div> </div>
</div> </div>
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" /> <NoteOptions event={event} className="shrink-0 [&_svg]:size-5" />
</div> </div>
<EventPowLabel event={event} className="mt-0.5" />
{webReactionParentUrl ? ( {webReactionParentUrl ? (
<div className="not-prose mt-1.5 max-w-full" data-parent-note-preview> <div className="not-prose mt-1.5 max-w-full" data-parent-note-preview>
<WebPreview url={webReactionParentUrl} className="w-full" /> <WebPreview url={webReactionParentUrl} className="w-full" />

21
src/components/Tabs/index.tsx

@ -1,5 +1,5 @@
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -40,7 +40,7 @@ export default function Tabs({
const activeTab = tabRefs.current[activeIndex] const activeTab = tabRefs.current[activeIndex]
const tabsContainer = tabsContainerRef.current const tabsContainer = tabsContainerRef.current
const { offsetWidth, offsetLeft, offsetHeight } = activeTab const { offsetWidth, offsetLeft, offsetHeight } = activeTab
const padding = 24 // 12px padding on each side const padding = Math.min(24, Math.max(8, offsetWidth * 0.12))
// Get the container's top position relative to the viewport // Get the container's top position relative to the viewport
const containerTop = tabsContainer.getBoundingClientRect().top const containerTop = tabsContainer.getBoundingClientRect().top
@ -124,15 +124,15 @@ export default function Tabs({
<div <div
ref={containerRef} ref={containerRef}
className={cn( className={cn(
'sticky flex justify-between top-12 bg-background z-30 px-1 w-full transition-transform border-b', 'sticky top-12 z-30 flex w-full min-w-0 items-end justify-between border-b bg-background px-1 transition-transform',
deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : '' deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : ''
)} )}
> >
<div className="flex-1 w-0 min-w-0"> <div className="min-w-0 w-0 flex-1">
<div <div
ref={tabsContainerRef} ref={tabsContainerRef}
role="tablist" role="tablist"
className="flex relative gap-1 overflow-x-auto scrollbar-hide" className="relative flex gap-0.5 overflow-x-auto overscroll-x-contain scrollbar-hide sm:gap-1"
> >
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<button <button
@ -144,9 +144,8 @@ export default function Tabs({
tabRefs.current[index] = el tabRefs.current[index] = el
}} }}
className={cn( className={cn(
'text-center py-2 px-2 sm:px-4 md:px-6 font-semibold whitespace-nowrap rounded-lg text-xs sm:text-sm md:text-base shrink-0 flex items-center gap-2 justify-center', 'flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg border-0 bg-transparent px-1.5 py-1.5 text-center text-xs font-semibold shadow-none transition-colors sm:gap-2 sm:px-3 sm:py-2 sm:text-sm md:px-5 md:text-base',
'bg-transparent border-0 shadow-none cursor-pointer transition-colors', 'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
value === tab.value ? '' : 'text-muted-foreground' value === tab.value ? '' : 'text-muted-foreground'
)} )}
onClick={() => { onClick={() => {
@ -158,7 +157,7 @@ export default function Tabs({
</button> </button>
))} ))}
<div <div
className="absolute h-1 bg-primary rounded-full transition-all duration-500" className="absolute h-1 rounded-full bg-primary transition-all duration-500"
style={{ style={{
width: `${indicatorStyle.width}px`, width: `${indicatorStyle.width}px`,
left: `${indicatorStyle.left}px`, left: `${indicatorStyle.left}px`,
@ -167,7 +166,9 @@ export default function Tabs({
/> />
</div> </div>
</div> </div>
{options && <div className="py-1 flex items-center shrink-0 gap-1">{options}</div>} {options ? (
<div className="flex shrink-0 items-center gap-0 py-1 pl-0.5">{options}</div>
) : null}
</div> </div>
) )
} }

6
src/components/ZapDialog/PostPaymentMessagePrompt.tsx

@ -140,7 +140,7 @@ export default function PostPaymentMessagePrompt({
<DialogDescription className="sr-only">{t('Post payment prompt label')}</DialogDescription> <DialogDescription className="sr-only">{t('Post payment prompt label')}</DialogDescription>
) : null} ) : null}
</DrawerHeader> </DrawerHeader>
<div className="min-w-0 overflow-x-hidden px-0 pb-4">{body}</div> <div className="min-w-0 px-px pb-4">{body}</div>
{step === 'choice' ? ( {step === 'choice' ? (
<DrawerFooter className={choiceFooterClass}>{choiceActions}</DrawerFooter> <DrawerFooter className={choiceFooterClass}>{choiceActions}</DrawerFooter>
) : null} ) : null}
@ -152,7 +152,7 @@ export default function PostPaymentMessagePrompt({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent <DialogContent
className="flex w-[calc(100vw-1.25rem)] max-w-lg min-w-0 flex-col gap-4 overflow-hidden sm:max-w-lg" className="flex w-[calc(100vw-1.25rem)] max-w-lg min-w-0 flex-col gap-4 max-h-[min(92dvh,900px)] overflow-y-auto sm:max-w-lg"
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
> >
<DialogHeader className="min-w-0 shrink-0"> <DialogHeader className="min-w-0 shrink-0">
@ -161,7 +161,7 @@ export default function PostPaymentMessagePrompt({
<DialogDescription className="sr-only">{t('Post payment prompt label')}</DialogDescription> <DialogDescription className="sr-only">{t('Post payment prompt label')}</DialogDescription>
) : null} ) : null}
</DialogHeader> </DialogHeader>
<div className="min-w-0 overflow-x-hidden">{body}</div> <div className="min-w-0 px-px">{body}</div>
{step === 'choice' ? ( {step === 'choice' ? (
<DialogFooter className={choiceFooterClass}>{choiceActions}</DialogFooter> <DialogFooter className={choiceFooterClass}>{choiceActions}</DialogFooter>
) : null} ) : null}

20
src/components/ZapDialog/PublicMessageForm.tsx

@ -94,15 +94,17 @@ export default function PublicMessageForm({
return ( return (
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm text-muted-foreground">{t('Tip notice prompt description')}</p> <p className="text-sm text-muted-foreground">{t('Tip notice prompt description')}</p>
<Textarea <div className="mt-3 min-w-0 max-w-full">
ref={textareaRef} <Textarea
value={message} ref={textareaRef}
onChange={(e) => setMessage(e.target.value)} value={message}
disabled={sending} onChange={(e) => setMessage(e.target.value)}
rows={6} disabled={sending}
className="mt-3 min-h-[10rem] resize-y text-sm leading-relaxed" rows={6}
aria-label={t('Tip notice prompt description')} className="min-h-[10rem] w-full max-w-full resize-y box-border text-sm leading-relaxed focus-visible:ring-inset"
/> aria-label={t('Tip notice prompt description')}
/>
</div>
{previewEvent ? ( {previewEvent ? (
<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>

22
src/components/ZapDialog/SuperchatRequestForm.tsx

@ -129,16 +129,18 @@ export default function SuperchatRequestForm({
{t('Superchat estimated amount hint')} {t('Superchat estimated amount hint')}
</p> </p>
</div> </div>
<Textarea <div className="mt-3 min-w-0 max-w-full">
ref={textareaRef} <Textarea
value={message} ref={textareaRef}
onChange={(e) => setMessage(e.target.value)} value={message}
disabled={sending} onChange={(e) => setMessage(e.target.value)}
rows={5} disabled={sending}
className="mt-3 min-h-[8rem] resize-y text-sm leading-relaxed" rows={5}
aria-label={t('Superchat message')} className="min-h-[8rem] w-full max-w-full resize-y box-border text-sm leading-relaxed focus-visible:ring-inset"
placeholder={t('Superchat message placeholder')} aria-label={t('Superchat message')}
/> placeholder={t('Superchat message placeholder')}
/>
</div>
<div className="mt-4 grid gap-2"> <div className="mt-4 grid gap-2">
<Label htmlFor="superchat-pow">{t('Proof of Work (difficulty {{minPow}})', { minPow })}</Label> <Label htmlFor="superchat-pow">{t('Proof of Work (difficulty {{minPow}})', { minPow })}</Label>
<Slider <Slider

7
src/constants.ts

@ -972,7 +972,14 @@ export const PROFILE_MEDIA_TAB_KINDS: readonly number[] = [
ExtendedKind.VOICE ExtendedKind.VOICE
] ]
/** Home feed Gallery tab: picture + NIP-71 video only (20, 21, 22, 34235). */
export const HOME_GALLERY_TAB_KINDS: readonly number[] = [
ExtendedKind.PICTURE,
...NIP71_VIDEO_KINDS
]
const PROFILE_MEDIA_TAB_KIND_SET = new Set<number>(PROFILE_MEDIA_TAB_KINDS) const PROFILE_MEDIA_TAB_KIND_SET = new Set<number>(PROFILE_MEDIA_TAB_KINDS)
export const HOME_GALLERY_TAB_KIND_SET = new Set<number>(HOME_GALLERY_TAB_KINDS)
/** /**
* Kinds subscribed on the profile Posts tab only. Omits publication kinds and native media kinds so those * Kinds subscribed on the profile Posts tab only. Omits publication kinds and native media kinds so those

3
src/lib/console-log-buffer.ts

@ -82,6 +82,9 @@ function captureLog(type: string, ...args: unknown[]) {
if (message.includes('NOTICE from')) { if (message.includes('NOTICE from')) {
return return
} }
if (import.meta.env.DEV && message.includes('[feed:')) {
return
}
buffer.push({ type, message, formattedParts, timestamp: Date.now() }) buffer.push({ type, message, formattedParts, timestamp: Date.now() })
if (buffer.length > MAX_ENTRIES) { if (buffer.length > MAX_ENTRIES) {
buffer.splice(0, buffer.length - MAX_ENTRIES) buffer.splice(0, buffer.length - MAX_ENTRIES)

31
src/lib/error-suppression.ts

@ -103,6 +103,37 @@ function isExpectedDevAppNoise(message: string): boolean {
if (message.includes('[vite]') && (message.includes('connected') || message.includes('connecting'))) { if (message.includes('[vite]') && (message.includes('connected') || message.includes('connecting'))) {
return true return true
} }
if (message.includes('[feed:')) {
return true
}
if (
message.includes('[SpellsPage] Spell feed') ||
message.includes('[NIP-42] Auth accepted') ||
message.includes('[RelayInfo] NIP-11 received') ||
message.includes('[client] Prewarm:') ||
message.includes('[RssFeedSettingsPage] Loaded RSS feed list')
) {
return true
}
if (
message.includes('localhost:4869') ||
message.includes('127.0.0.1:4869') ||
message.includes('ws://localhost:4869')
) {
if (
message.includes('[PublishEvent]') ||
message.includes('[Publish]') ||
message.includes('[RelayOp]') ||
message.includes('connection failed') ||
message.includes('connection timed out') ||
message.includes('Local relay connection timeout')
) {
return true
}
}
if (message.includes('[FetchRelayLists] Network relay-list fetch exceeded budget')) {
return true
}
return false return false
} }

12
src/services/client-events.service.ts

@ -232,11 +232,13 @@ export class EventService {
const snapshot = [...waiters] const snapshot = [...waiters]
this.sessionEventWaiters.delete(hexId) this.sessionEventWaiters.delete(hexId)
for (const cb of snapshot) { for (const cb of snapshot) {
try { queueMicrotask(() => {
cb() try {
} catch (e) { cb()
logger.warn('[EventService] sessionEventWaiter failed', { hexId: hexId.slice(0, 8), e }) } catch (e) {
} logger.warn('[EventService] sessionEventWaiter failed', { hexId: hexId.slice(0, 8), e })
}
})
} }
} }

Loading…
Cancel
Save