diff --git a/package-lock.json b/package-lock.json
index cd570f65..67f4f81f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index c9e91a93..03108a21 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/Note/NotificationEventCard.tsx b/src/components/Note/NotificationEventCard.tsx
index c55f9191..444bc5eb 100644
--- a/src/components/Note/NotificationEventCard.tsx
+++ b/src/components/Note/NotificationEventCard.tsx
@@ -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 (
-
-
-
- {reactionDisplay}
-
-
{t('Notification reaction summary')}
-
-
- )
- }
-
if (event.kind === kinds.Repost) {
return (
setShowMuted(true)} />
} else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) {
content =
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 =
} else if (event.kind === kinds.Highlights) {
// Try to render the Highlight component with error boundary
@@ -275,8 +273,34 @@ export default function Note({
}}
>
-
- {isSyntheticRssParent ? (
+
+ {event.kind === kinds.Reaction ? (
+
+
+ {reactionDisplayEmoji(event)}
+
+
+
+
+
+
+ {t('Notification reaction summary')}
+
+
+
+
+ ) : isSyntheticRssParent ? (
<>
([])
const [selectedIndex, setSelectedIndex] = useState(-1)
const searchInputRef = useRef
(null)
+ const barContainerRef = useRef(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<
}
}, [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<
[input, onSearch, selectableOptions, selectedIndex]
)
+ const suggestTopPx = Math.max(0, suggestPanelTop - 4)
+ const suggestionsPanel = list ? (
+ e.preventDefault()}
+ >
+
{list}
+
+ ) : null
+
return (
-
- {displayList && list && (
+
+ {displayList && list && !isSmallScreen && (
<>
+ {suggestionsPanel}
e.preventDefault()}
- >
-
{list}
-
-
blur()} />
+ className="fixed inset-0 z-40 w-full h-full"
+ onClick={() => blur()}
+ aria-hidden
+ />
+ >
+ )}
+ {displayList && list && isSmallScreen && (
+ <>
+
blur()}
+ aria-hidden
+ />
+ {suggestionsPanel}
>
)}
{children}
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 846b806c..4c2fb347 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -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.',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 293e72eb..80b0b148 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -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.',
diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx
index 26fd878c..5d4d3e25 100644
--- a/src/pages/primary/SpellsPage/index.tsx
+++ b/src/pages/primary/SpellsPage/index.tsx
@@ -320,6 +320,8 @@ const SpellsPage = forwardRef(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])