Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
77e129de7a
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 40
      src/components/Note/NotificationEventCard.tsx
  4. 40
      src/components/Note/index.tsx
  5. 77
      src/components/SearchBar/index.tsx
  6. 2
      src/components/ui/select.tsx
  7. 2
      src/i18n/locales/de.ts
  8. 2
      src/i18n/locales/en.ts
  9. 2
      src/pages/primary/SpellsPage/index.tsx

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
{
"name": "jumble-imwald",
"version": "19.3.1",
"version": "19.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jumble-imwald",
"version": "19.3.1",
"version": "19.3.3",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "jumble-imwald",
"version": "19.3.1",
"version": "19.3.3",
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true,
"type": "module",

40
src/components/Note/NotificationEventCard.tsx

@ -1,42 +1,24 @@ @@ -1,42 +1,24 @@
import { ExtendedKind } from '@/constants'
import { cn } from '@/lib/utils'
import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
/** Reaction `content` as display emoji/text (NIP-25); empty content → heart. */
export function reactionDisplayEmoji(event: Event): string {
if (event.kind !== kinds.Reaction) return ''
const raw = event.content?.trim() ?? ''
if (!raw) return '❤'
if (raw.length > 64) return `${raw.slice(0, 64)}`
return raw
}
/**
* Compact card for interaction events in notification-style feeds (reactions, boosts, poll votes).
* The surrounding {@link Note} row still shows author + {@link ParentNotePreview} for the target.
* Compact card for interaction events in notification-style feeds (boosts, poll votes).
* Reactions use a one-line header in {@link Note} (emoji + user + blurb) instead of this card.
*/
export default function NotificationEventCard({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
const reactionDisplay = useMemo(() => {
if (event.kind !== kinds.Reaction) return null
const raw = event.content?.trim() ?? ''
if (!raw) return '❤'
if (raw.length > 64) return `${raw.slice(0, 64)}`
return raw
}, [event.content, event.kind])
if (event.kind === kinds.Reaction) {
return (
<div
className={cn(
'rounded-lg border border-border bg-card p-4 text-card-foreground shadow-sm',
className
)}
>
<div className="flex flex-wrap items-center gap-3">
<span className="text-3xl leading-none select-none" aria-hidden>
{reactionDisplay}
</span>
<p className="text-sm text-muted-foreground">{t('Notification reaction summary')}</p>
</div>
</div>
)
}
if (event.kind === kinds.Repost) {
return (
<div

40
src/components/Note/index.tsx

@ -41,7 +41,7 @@ import MutedNote from './MutedNote' @@ -41,7 +41,7 @@ import MutedNote from './MutedNote'
import NsfwNote from './NsfwNote'
import PictureNote from './PictureNote'
import Poll from './Poll'
import NotificationEventCard from './NotificationEventCard'
import NotificationEventCard, { reactionDisplayEmoji } from './NotificationEventCard'
import UnknownNote from './UnknownNote'
import VideoNote from './VideoNote'
import RelayReview from './RelayReview'
@ -130,11 +130,9 @@ export default function Note({ @@ -130,11 +130,9 @@ export default function Note({
content = <MutedNote show={() => setShowMuted(true)} />
} else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) {
content = <NsfwNote show={() => setShowNsfw(true)} />
} else if (
event.kind === kinds.Reaction ||
event.kind === kinds.Repost ||
event.kind === ExtendedKind.POLL_RESPONSE
) {
} else if (event.kind === kinds.Reaction) {
content = null
} else if (event.kind === kinds.Repost || event.kind === ExtendedKind.POLL_RESPONSE) {
content = <NotificationEventCard className="mt-2" event={event} />
} else if (event.kind === kinds.Highlights) {
// Try to render the Highlight component with error boundary
@ -275,8 +273,34 @@ export default function Note({ @@ -275,8 +273,34 @@ export default function Note({
}}
>
<div className="flex justify-between items-start gap-2">
<div className="flex items-center space-x-2 flex-1">
{isSyntheticRssParent ? (
<div className="flex min-w-0 flex-1 items-center gap-2">
{event.kind === kinds.Reaction ? (
<div className="flex min-w-0 flex-1 flex-nowrap items-center gap-2">
<span
className="shrink-0 text-2xl leading-none select-none"
aria-hidden
>
{reactionDisplayEmoji(event)}
</span>
<UserAvatar userId={event.pubkey} size={size === 'small' ? 'medium' : 'normal'} />
<div className="flex min-w-0 flex-1 flex-nowrap items-center gap-2 overflow-hidden">
<Username
userId={event.pubkey}
className={`max-w-[min(12rem,40vw)] shrink font-semibold truncate ${size === 'small' ? 'text-sm' : ''}`}
skeletonClassName={size === 'small' ? 'h-3' : 'h-4'}
/>
<ClientTag event={event} />
<span className="min-w-0 flex-1 truncate text-sm text-muted-foreground">
{t('Notification reaction summary')}
</span>
</div>
<FormattedTimestamp
timestamp={event.created_at}
className="shrink-0 text-sm text-muted-foreground"
short={isSmallScreen}
/>
</div>
) : isSyntheticRssParent ? (
<>
<div
className={`shrink-0 rounded-full bg-muted overflow-hidden flex items-center justify-center ${

77
src/components/SearchBar/index.tsx

@ -19,6 +19,7 @@ import { @@ -19,6 +19,7 @@ import {
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState
@ -45,6 +46,8 @@ const SearchBar = forwardRef< @@ -45,6 +46,8 @@ const SearchBar = forwardRef<
const [selectableOptions, setSelectableOptions] = useState<TSearchParams[]>([])
const [selectedIndex, setSelectedIndex] = useState(-1)
const searchInputRef = useRef<HTMLInputElement>(null)
const barContainerRef = useRef<HTMLDivElement>(null)
const [suggestPanelTop, setSuggestPanelTop] = useState(0)
const normalizedUrl = useMemo(() => {
if (['w', 'ws', 'ws:', 'ws:/', 'wss', 'wss:', 'wss:/'].includes(input)) {
return undefined
@ -260,6 +263,24 @@ const SearchBar = forwardRef< @@ -260,6 +263,24 @@ const SearchBar = forwardRef<
}
}, [displayList, list])
const updateSuggestPanelGeometry = useCallback(() => {
const el = barContainerRef.current
if (!el) return
setSuggestPanelTop(el.getBoundingClientRect().bottom)
}, [])
useLayoutEffect(() => {
if (!displayList || !list || !isSmallScreen) return
updateSuggestPanelGeometry()
const onScrollOrResize = () => updateSuggestPanelGeometry()
window.addEventListener('scroll', onScrollOrResize, true)
window.addEventListener('resize', onScrollOrResize)
return () => {
window.removeEventListener('scroll', onScrollOrResize, true)
window.removeEventListener('resize', onScrollOrResize)
}
}, [displayList, list, isSmallScreen, input, updateSuggestPanelGeometry])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
@ -298,29 +319,57 @@ const SearchBar = forwardRef< @@ -298,29 +319,57 @@ const SearchBar = forwardRef<
[input, onSearch, selectableOptions, selectedIndex]
)
const suggestTopPx = Math.max(0, suggestPanelTop - 4)
const suggestionsPanel = list ? (
<div
className={cn(
'bg-surface-background shadow-lg',
isSmallScreen
? 'fixed left-4 right-4 z-[110] overflow-y-auto rounded-b-lg border border-t-0 border-border/80 pt-1'
: 'absolute top-full z-50 -translate-y-1 inset-x-0 rounded-b-lg pt-1'
)}
style={
isSmallScreen
? {
top: suggestTopPx,
maxHeight: `calc(100dvh - ${suggestTopPx}px - 3.25rem - env(safe-area-inset-bottom, 0px))`
}
: undefined
}
onMouseDown={(e) => e.preventDefault()}
>
<div className="h-fit">{list}</div>
</div>
) : null
return (
<div className="relative flex gap-1 items-center h-full w-full">
{displayList && list && (
<div ref={barContainerRef} className="relative flex gap-1 items-center h-full w-full">
{displayList && list && !isSmallScreen && (
<>
{suggestionsPanel}
<div
className={cn(
'bg-surface-background rounded-b-lg shadow-lg z-50',
isSmallScreen
? 'absolute top-full -translate-y-1 inset-x-0 pt-1'
: 'absolute top-full -translate-y-1 inset-x-0 pt-1 '
)}
onMouseDown={(e) => e.preventDefault()}
>
<div className="h-fit">{list}</div>
</div>
<div className="fixed inset-0 w-full h-full" onClick={() => blur()} />
className="fixed inset-0 z-40 w-full h-full"
onClick={() => blur()}
aria-hidden
/>
</>
)}
{displayList && list && isSmallScreen && (
<>
<div
className="fixed inset-0 z-[100] w-full h-full"
onClick={() => blur()}
aria-hidden
/>
{suggestionsPanel}
</>
)}
<SearchInput
ref={searchInputRef}
className={cn(
'bg-surface-background shadow-inner h-full border-none',
searching ? 'z-50' : ''
searching && isSmallScreen && 'relative z-[120]',
searching && !isSmallScreen && 'z-50'
)}
placeholder={t('People, keywords, or relays')}
value={input}

2
src/components/ui/select.tsx

@ -79,7 +79,7 @@ const SelectContent = React.forwardRef< @@ -79,7 +79,7 @@ const SelectContent = React.forwardRef<
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
'max-h-[min(24rem,var(--radix-select-content-available-height,80vh))] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
{children}

2
src/i18n/locales/de.ts

@ -376,7 +376,7 @@ export default { @@ -376,7 +376,7 @@ export default {
Topics: 'Themen',
'Open in a': 'Öffnen in {{a}}',
'Cannot handle event of kind k': 'Ereignis des Typs {{k}} kann nicht verarbeitet werden',
'Notification reaction summary': 'Hat auf die Notiz darüber reagiert.',
'Notification reaction summary': 'hat auf diese Notiz reagiert.',
'Notification boost summary': 'Hat diese Notiz geboostet',
'Notification boost detail': 'Die Vorschau darüber ist der Originalbeitrag.',
'Notification poll vote summary': 'Hat an der Umfrage darüber teilgenommen.',

2
src/i18n/locales/en.ts

@ -369,7 +369,7 @@ export default { @@ -369,7 +369,7 @@ export default {
Topics: 'Topics',
'Open in a': 'Open in {{a}}',
'Cannot handle event of kind k': 'Cannot handle event of kind {{k}}',
'Notification reaction summary': 'Reacted to the note above.',
'Notification reaction summary': 'reacted to this note.',
'Notification boost summary': 'Boosted this note',
'Notification boost detail': 'The preview above is the original post.',
'Notification poll vote summary': 'Voted on the poll above.',

2
src/pages/primary/SpellsPage/index.tsx

@ -320,6 +320,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -320,6 +320,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
setSelectedSpell(null)
} else {
urlFauxSpellInstrumentedRef.current = null
// URL / props no longer name a faux spell (e.g. bottom bar “Spells” → `/spells`) — leave the feed.
setSelectedFauxSpell(null)
}
}, [spellProp, logSpellFeedPickerSelection])

Loading…
Cancel
Save