Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
65ae5a72ae
  1. 2
      src/components/BookmarkButton/index.tsx
  2. 12
      src/components/Note/index.tsx
  3. 1
      src/components/NoteCard/MainNoteCard.tsx
  4. 18
      src/components/NoteOptions/DesktopMenu.tsx
  5. 33
      src/components/NoteOptions/MobileMenu.tsx
  6. 85
      src/components/NoteOptions/NoteOptionsMetaHeader.tsx
  7. 21
      src/components/NoteOptions/index.tsx
  8. 6
      src/components/NoteStats/LikeButton.tsx
  9. 2
      src/components/NoteStats/ReplyButton.tsx
  10. 6
      src/components/NoteStats/RepostButton.tsx
  11. 46
      src/components/NoteStats/SeenOnButton.tsx
  12. 6
      src/components/NoteStats/ZapButton.tsx
  13. 10
      src/components/NoteStats/index.tsx
  14. 18
      src/components/ReplyNote/index.tsx
  15. 46
      src/hooks/useSeenOnRelays.ts

2
src/components/BookmarkButton/index.tsx

@ -68,7 +68,7 @@ export default function BookmarkButton({ event }: { event: Event }) { @@ -68,7 +68,7 @@ export default function BookmarkButton({ event }: { event: Event }) {
<button
className={`flex items-center gap-1 ${
isBookmarked ? 'text-rose-400' : 'text-muted-foreground'
} enabled:hover:text-rose-400 px-3 h-full`}
} enabled:hover:text-rose-400 px-1.5 h-full`}
onClick={isBookmarked ? handleRemoveBookmark : handleBookmark}
disabled={updating}
title={isBookmarked ? t('Remove bookmark') : t('Bookmark')}

12
src/components/Note/index.tsx

@ -74,7 +74,6 @@ import Poll from './Poll' @@ -74,7 +74,6 @@ import Poll from './Poll'
import NotificationEventCard from './NotificationEventCard'
import ReactionEmojiDisplay from './ReactionEmojiDisplay'
import UnknownNote from './UnknownNote'
import NoteKindLabel from './NoteKindLabel'
import { Button } from '@/components/ui/button'
import VideoNote from './VideoNote'
import RelayReview from './RelayReview'
@ -230,7 +229,8 @@ export default function Note({ @@ -230,7 +229,8 @@ export default function Note({
/** When true, parent list already prefetches embeds — skip per-row duplicate fetches. */
skipEmbedPrefetch = false,
showPaymentAttestationAction = false,
pinned = false
pinned = false,
seenOnAllowlist
}: {
event: Event
originalNoteId?: string
@ -252,6 +252,8 @@ export default function Note({ @@ -252,6 +252,8 @@ export default function Note({
skipEmbedPrefetch?: boolean
/** Notifications feed: show attest-superchat action on incoming payments. */
showPaymentAttestationAction?: boolean
/** When set (home favorites feed), relay list in ⋯ menu matches the feed allowlist. */
seenOnAllowlist?: readonly string[]
}) {
const { t } = useTranslation()
const { navigateToNote } = useSmartNoteNavigationOptional()
@ -786,6 +788,7 @@ export default function Note({ @@ -786,6 +788,7 @@ export default function Note({
<NoteOptions
event={event}
pinned={pinned}
seenOnAllowlist={seenOnAllowlist}
className={cn(
'py-1 shrink-0',
size === 'small' ? '[&_svg]:size-4' : '[&_svg]:size-5'
@ -808,10 +811,7 @@ export default function Note({ @@ -808,10 +811,7 @@ export default function Note({
)}
</div>
</div>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">
<NoteKindLabel kind={event.kind} event={event} size={size} />
<EventPowLabel event={event} />
</div>
<EventPowLabel event={event} className="mt-1" />
{webReactionParentUrl ? (
<div className="mt-2 not-prose max-w-full" data-parent-note-preview>
<WebPreview url={webReactionParentUrl} className="w-full" />

1
src/components/NoteCard/MainNoteCard.tsx

@ -149,6 +149,7 @@ function MainNoteCard({ @@ -149,6 +149,7 @@ function MainNoteCard({
skipEmbedPrefetch={deferAuthorAvatar}
showPaymentAttestationAction={showPaymentAttestationAction}
pinned={pinned}
seenOnAllowlist={seenOnAllowlist}
/>
</Collapsible>
{!embedded && !searchListPreview ? <NoteBoostBadges event={event} className={`mt-2 ${notePadX}`} /> : null}

18
src/components/NoteOptions/DesktopMenu.tsx

@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next' @@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'
interface DesktopMenuProps {
menuActions: MenuAction[]
trigger: React.ReactNode
header?: React.ReactNode
}
function filterSubMenuRows(
@ -136,17 +137,20 @@ const MenuContent = memo( @@ -136,17 +137,20 @@ const MenuContent = memo(
)
MenuContent.displayName = 'MenuContent'
export function DesktopMenu({ menuActions, trigger }: DesktopMenuProps) {
export function DesktopMenu({ menuActions, trigger, header }: DesktopMenuProps) {
const [subMenuFilter, setSubMenuFilter] = useState('')
return (
<DropdownMenu onOpenChange={(open) => !open && setSubMenuFilter('')}>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent className="max-h-[50vh] overflow-y-auto">
<MenuContent
menuActions={menuActions}
subMenuFilter={subMenuFilter}
setSubMenuFilter={setSubMenuFilter}
/>
<DropdownMenuContent className="max-h-[50vh] overflow-y-auto p-0">
{header}
<div className="py-1">
<MenuContent
menuActions={menuActions}
subMenuFilter={subMenuFilter}
setSubMenuFilter={setSubMenuFilter}
/>
</div>
</DropdownMenuContent>
</DropdownMenu>
)

33
src/components/NoteOptions/MobileMenu.tsx

@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next' @@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
interface MobileMenuProps {
menuActions: MenuAction[]
trigger: React.ReactNode
header?: React.ReactNode
isDrawerOpen: boolean
setIsDrawerOpen: (open: boolean) => void
showSubMenu: boolean
@ -33,6 +34,7 @@ function filterSubMenuRows( @@ -33,6 +34,7 @@ function filterSubMenuRows(
export function MobileMenu({
menuActions,
trigger,
header,
isDrawerOpen,
setIsDrawerOpen,
showSubMenu,
@ -63,20 +65,23 @@ export function MobileMenu({ @@ -63,20 +65,23 @@ export function MobileMenu({
</DrawerHeader>
<div className="overflow-y-auto overscroll-contain py-2" style={{ touchAction: 'pan-y' }}>
{!showSubMenu ? (
menuActions.map((action, index) => {
const Icon = action.icon
return (
<Button
key={index}
onClick={action.onClick}
className={`w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5 ${action.className || ''}`}
variant="ghost"
>
<Icon />
{action.label}
</Button>
)
})
<>
{header}
{menuActions.map((action, index) => {
const Icon = action.icon
return (
<Button
key={index}
onClick={action.onClick}
className={`w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5 ${action.className || ''}`}
variant="ghost"
>
<Icon />
{action.label}
</Button>
)
})}
</>
) : (
<>
<Button

85
src/components/NoteOptions/NoteOptionsMetaHeader.tsx

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
import RelayIcon from '@/components/RelayIcon'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { useSeenOnRelays } from '@/hooks/useSeenOnRelays'
import { getKindDescription } from '@/lib/kind-description'
import { toRelay } from '@/lib/link'
import { simplifyUrl } from '@/lib/url'
import { useSecondaryPage } from '@/PageManager'
import type { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
export default function NoteOptionsMetaHeader({
event,
allowedRelays,
onNavigate,
inDropdown = false
}: {
event: Event
allowedRelays?: readonly string[]
onNavigate?: () => void
inDropdown?: boolean
}) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const relays = useSeenOnRelays(event.id, allowedRelays)
const { description } = getKindDescription(event.kind, event)
const relayRows = relays.map((relay) => {
const label = (
<>
<RelayIcon url={relay} className="size-4 shrink-0" />
<span className="min-w-0 truncate">{simplifyUrl(relay)}</span>
</>
)
if (inDropdown) {
return (
<DropdownMenuItem
key={relay}
className="min-w-0 gap-2"
onSelect={() => {
onNavigate?.()
push(toRelay(relay))
}}
>
{label}
</DropdownMenuItem>
)
}
return (
<li key={relay}>
<button
type="button"
className="flex w-full min-w-0 items-center gap-2 rounded-md px-1 py-1 text-left text-sm text-foreground hover:bg-muted"
onClick={() => {
onNavigate?.()
push(toRelay(relay))
}}
>
{label}
</button>
</li>
)
})
return (
<div className="space-y-2 border-b border-border px-3 py-2.5">
<p className="text-xs leading-snug text-muted-foreground/80" data-note-kind-label>
{t('Note kind label line', { kind: event.kind, description })}
</p>
{relays.length > 0 ? (
<div className="space-y-1">
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
{t('Seen on')}
</p>
{inDropdown ? (
<div className="space-y-0.5">{relayRows}</div>
) : (
<ul className="max-h-32 space-y-0.5 overflow-y-auto overscroll-y-contain">{relayRows}</ul>
)}
</div>
) : null}
</div>
)
}

21
src/components/NoteOptions/index.tsx

@ -10,6 +10,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -10,6 +10,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { DesktopMenu } from './DesktopMenu'
import EditOrCloneEventDialog, { type TEditOrCloneMode } from './EditOrCloneEventDialog'
import { MobileMenu } from './MobileMenu'
import NoteOptionsMetaHeader from './NoteOptionsMetaHeader'
import RawEventDialog from './RawEventDialog'
import ReportDialog from './ReportDialog'
import { SubMenuAction, useMenuActions, type ShowSubMenuOptions } from './useMenuActions'
@ -27,12 +28,15 @@ export default function NoteOptions({ @@ -27,12 +28,15 @@ export default function NoteOptions({
initialPublicMessageTo,
onOpenCallInvite,
initialDefaultContent,
pinned = false
pinned = false,
seenOnAllowlist
}: {
event: Event
className?: string
/** Note is shown in a pinned section (profile pins, etc.). */
pinned?: boolean
/** When set (home favorites feed), relay list in the menu matches the feed allowlist. */
seenOnAllowlist?: readonly string[]
initialHighlightData?: HighlightData
highlightDefaultContent?: string
isPostEditorOpen?: boolean
@ -124,12 +128,25 @@ export default function NoteOptions({ @@ -124,12 +128,25 @@ export default function NoteOptions({
[]
)
const menuHeader = useMemo(
() => (
<NoteOptionsMetaHeader
event={event}
allowedRelays={seenOnAllowlist}
onNavigate={closeDrawer}
inDropdown={!isSmallScreen}
/>
),
[event, seenOnAllowlist, isSmallScreen]
)
return (
<div className={className} onClick={(e) => e.stopPropagation()}>
{isSmallScreen ? (
<MobileMenu
menuActions={menuActions}
trigger={trigger}
header={menuHeader}
isDrawerOpen={isDrawerOpen}
setIsDrawerOpen={setIsDrawerOpen}
showSubMenu={showSubMenu}
@ -140,7 +157,7 @@ export default function NoteOptions({ @@ -140,7 +157,7 @@ export default function NoteOptions({
goBackToMainMenu={goBackToMainMenu}
/>
) : (
<DesktopMenu menuActions={menuActions} trigger={trigger} />
<DesktopMenu menuActions={menuActions} trigger={trigger} header={menuHeader} />
)}
<RawEventDialog

6
src/components/NoteStats/LikeButton.tsx

@ -244,7 +244,7 @@ export function LikeButtonWithStats({ @@ -244,7 +244,7 @@ export function LikeButtonWithStats({
const likeIconButton = (
<button
type="button"
className="flex h-full items-center gap-1 pl-3 pr-1 text-muted-foreground enabled:hover:text-primary"
className="flex h-full items-center gap-0.5 px-1.5 text-muted-foreground enabled:hover:text-primary"
title={t('Like')}
disabled={liking}
onClick={openReactionPicker}
@ -261,12 +261,12 @@ export function LikeButtonWithStats({ @@ -261,12 +261,12 @@ export function LikeButtonWithStats({
const likeCountLabel = showLikeCount ? (
<ReactionCountHover noteStats={noteStats}>
<div className="pr-3 text-sm tabular-nums">
<div className="pr-1 text-sm tabular-nums">
{(likeCount ?? 0) >= 100 ? '99+' : String(likeCount ?? 0)}
</div>
</ReactionCountHover>
) : (
<span className="pr-3" aria-hidden />
<span className="pr-1" aria-hidden />
)
// Discussions (kind 11) and kind 1111 under a discussion: only +/- vote reactions

2
src/components/NoteStats/ReplyButton.tsx

@ -40,7 +40,7 @@ export function ReplyButtonWithStats({ event, hideCount = false, noteStats }: Re @@ -40,7 +40,7 @@ export function ReplyButtonWithStats({ event, hideCount = false, noteStats }: Re
<>
<button
className={cn(
'flex gap-1 items-center enabled:hover:text-blue-400 pr-3 h-full',
'flex gap-1 items-center enabled:hover:text-blue-400 px-1.5 h-full',
hasReplied ? 'text-blue-400' : 'text-muted-foreground'
)}
onClick={(e) => {

6
src/components/NoteStats/RepostButton.tsx

@ -103,7 +103,7 @@ export function RepostButtonWithStats({ event, hideCount = false, noteStats }: R @@ -103,7 +103,7 @@ export function RepostButtonWithStats({ event, hideCount = false, noteStats }: R
<button
type="button"
className={cn(
'flex h-full items-center enabled:hover:text-lime-500 pl-3 pr-1',
'flex h-full items-center enabled:hover:text-lime-500 px-1.5',
hasReposted ? 'text-lime-500' : 'text-muted-foreground'
)}
title={t('Boost')}
@ -119,10 +119,10 @@ export function RepostButtonWithStats({ event, hideCount = false, noteStats }: R @@ -119,10 +119,10 @@ export function RepostButtonWithStats({ event, hideCount = false, noteStats }: R
const countLabel = showRepostCount ? (
<BoostCountHover noteStats={noteStats}>
<div className="pr-3 text-sm tabular-nums">{formatCount(repostCount ?? 0)}</div>
<div className="pr-1 text-sm tabular-nums">{formatCount(repostCount ?? 0)}</div>
</BoostCountHover>
) : (
<span className="pr-3" aria-hidden />
<span className="pr-1" aria-hidden />
)
const postEditor = (

46
src/components/NoteStats/SeenOnButton.tsx

@ -9,14 +9,13 @@ import { @@ -9,14 +9,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useSeenOnRelays } from '@/hooks/useSeenOnRelays'
import { toRelay } from '@/lib/link'
import { filterRelaysToUserAllowlist } from '@/lib/relay-allowlist'
import { normalizeAnyRelayUrl, simplifyUrl } from '@/lib/url'
import { simplifyUrl } from '@/lib/url'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
import { Server } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
@ -31,41 +30,8 @@ export default function SeenOnButton({ @@ -31,41 +30,8 @@ export default function SeenOnButton({
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { push } = useSecondaryPage()
const [relays, setRelays] = useState<string[]>([])
const relays = useSeenOnRelays(event.id, allowedRelays)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const allowedRelaysRef = useRef(allowedRelays)
allowedRelaysRef.current = allowedRelays
const allowedRelaysKey = allowedRelays?.length
? [...allowedRelays]
.map((u) => normalizeAnyRelayUrl(u) || u.trim())
.filter(Boolean)
.sort()
.join('|')
: ''
useEffect(() => {
let cancelled = false
let attempts = 0
const maxAttempts = 20
const apply = () => {
const seenOn = client.getSeenEventRelayUrls(event.id)
const allowlist = allowedRelaysRef.current
const visible =
allowlist?.length ? filterRelaysToUserAllowlist(seenOn, allowlist) : seenOn
if (!cancelled) setRelays(visible)
return visible.length > 0
}
if (apply()) return
const id = setInterval(() => {
if (cancelled) return
attempts++
if (apply() || attempts >= maxAttempts) clearInterval(id)
}, 500)
return () => {
cancelled = true
clearInterval(id)
}
}, [event.id, allowedRelaysKey])
const trigger = (
<button
@ -79,7 +45,7 @@ export default function SeenOnButton({ @@ -79,7 +45,7 @@ export default function SeenOnButton({
}}
>
<Server />
{relays.length > 0 && <div className="text-sm">{relays.length}</div>}
{relays.length > 0 ? <span className="text-sm">{relays.length}</span> : null}
</button>
)
@ -103,7 +69,7 @@ export default function SeenOnButton({ @@ -103,7 +69,7 @@ export default function SeenOnButton({
setIsDrawerOpen(false)
setTimeout(() => {
push(toRelay(relay))
}, 50) // Timeout to allow the drawer to close before navigating
}, 50)
}}
>
<RelayIcon url={relay} /> {simplifyUrl(relay)}

6
src/components/NoteStats/ZapButton.tsx

@ -225,7 +225,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB @@ -225,7 +225,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
<button
type="button"
className={cn(
'group flex h-full items-center pl-3 pr-1',
'group flex h-full items-center px-1.5',
disable ? 'cursor-not-allowed' : 'cursor-pointer'
)}
title={zapButtonTitle}
@ -248,7 +248,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB @@ -248,7 +248,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
<ZapCountHover noteStats={noteStats}>
<div
className={cn(
'pr-3 text-sm tabular-nums',
'pr-1 text-sm tabular-nums',
hasZapped ? 'text-yellow-400' : 'text-muted-foreground'
)}
>
@ -256,7 +256,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB @@ -256,7 +256,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
</div>
</ZapCountHover>
) : (
<span className="pr-3" aria-hidden />
<span className="pr-1" aria-hidden />
)}
</div>
<ZapDialog

10
src/components/NoteStats/index.tsx

@ -15,7 +15,6 @@ import NotificationThreadWatchButtons from '../NotificationThreadWatchButtons' @@ -15,7 +15,6 @@ import NotificationThreadWatchButtons from '../NotificationThreadWatchButtons'
import { LikeButtonWithStats } from './LikeButton'
import { ReplyButtonWithStats } from './ReplyButton'
import { RepostButtonWithStats } from './RepostButton'
import SeenOnButton from './SeenOnButton'
import { ZapButtonWithStats } from './ZapButton'
export default function NoteStats({
@ -147,16 +146,13 @@ export default function NoteStats({ @@ -147,16 +146,13 @@ export default function NoteStats({
>
<div
className={cn(
'flex min-w-0 flex-wrap items-center justify-between gap-x-1 gap-y-2 [&_svg]:size-4 max-sm:[&_button]:pr-2',
'flex min-w-0 flex-nowrap items-center gap-0 overflow-x-auto overscroll-x-contain [&_svg]:size-[15px] [&_button]:px-1.5',
loading ? 'animate-pulse' : '',
classNames?.buttonBar
)}
>
<div className="flex min-w-0 flex-wrap items-center">{interactionButtons}</div>
<div className="flex shrink-0 flex-wrap items-center">
{utilityButtons}
<SeenOnButton event={event} allowedRelays={seenOnAllowlist} />
</div>
{interactionButtons}
{utilityButtons}
</div>
</div>
)

18
src/components/ReplyNote/index.tsx

@ -38,7 +38,6 @@ import ParentNotePreview from '../ParentNotePreview' @@ -38,7 +38,6 @@ import ParentNotePreview from '../ParentNotePreview'
import WebPreview from '../WebPreview'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import NoteKindLabel from '../Note/NoteKindLabel'
import Superchat from '../Note/Superchat'
import Zap from '../Note/Zap'
import MoneroTip from '../Note/MoneroTip'
@ -156,22 +155,7 @@ export default function ReplyNote({ @@ -156,22 +155,7 @@ export default function ReplyNote({
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" />
</div>
</div>
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1">
<NoteKindLabel
kind={event.kind}
event={event}
size="small"
className={cn(
(isNip25ReactionKind(event.kind) ||
event.kind === kinds.Zap ||
event.kind === ExtendedKind.PAYMENT_NOTIFICATION ||
event.kind === ExtendedKind.MONERO_TIP_DISCLOSURE ||
event.kind === ExtendedKind.MONERO_TIP_RECEIPT) &&
'opacity-60'
)}
/>
<EventPowLabel event={event} />
</div>
<EventPowLabel event={event} className="mt-0.5" />
{webReactionParentUrl ? (
<div className="mt-1.5 not-prose max-w-full" data-parent-note-preview>
<WebPreview url={webReactionParentUrl} className="w-full" />

46
src/hooks/useSeenOnRelays.ts

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
import { filterRelaysToUserAllowlist } from '@/lib/relay-allowlist'
import { normalizeAnyRelayUrl } from '@/lib/url'
import client from '@/services/client.service'
import { useEffect, useRef, useState } from 'react'
export function useSeenOnRelays(
eventId: string,
allowedRelays?: readonly string[]
): string[] {
const [relays, setRelays] = useState<string[]>([])
const allowedRelaysRef = useRef(allowedRelays)
allowedRelaysRef.current = allowedRelays
const allowedRelaysKey = allowedRelays?.length
? [...allowedRelays]
.map((u) => normalizeAnyRelayUrl(u) || u.trim())
.filter(Boolean)
.sort()
.join('|')
: ''
useEffect(() => {
let cancelled = false
let attempts = 0
const maxAttempts = 20
const apply = () => {
const seenOn = client.getSeenEventRelayUrls(eventId)
const allowlist = allowedRelaysRef.current
const visible =
allowlist?.length ? filterRelaysToUserAllowlist(seenOn, allowlist) : seenOn
if (!cancelled) setRelays(visible)
return visible.length > 0
}
if (apply()) return
const id = setInterval(() => {
if (cancelled) return
attempts++
if (apply() || attempts >= maxAttempts) clearInterval(id)
}, 500)
return () => {
cancelled = true
clearInterval(id)
}
}, [eventId, allowedRelaysKey])
return relays
}
Loading…
Cancel
Save