()
@@ -1442,17 +1447,22 @@ function parseMarkdownContent(
// Also update lastIndex immediately to prevent processing of patterns in this range
lastIndex = textEndIndex
} else if (hasTextOnSameLine || hasTextBefore || hasTextAfterOnSameLine) {
- // Hashtag is part of text - merge just this hashtag and text after it (avoids hard break after #hashtag at start of line)
+ // Hashtag is part of text - merge this hashtag and all following hashtags/text on same line (avoids hard break between #hashtag #other)
const patternMarkdown = content.substring(pattern.index, pattern.end)
const textAfterPattern = content.substring(pattern.end, lineEndIndex)
text = text + patternMarkdown + textAfterPattern
textEndIndex = lineEndIndex === content.length ? content.length : lineEndIndex + 1
- const tag = pattern.data
- const tagLower = tag.toLowerCase()
- hashtagsInContent.add(tagLower)
- // Mark as merged BEFORE processing text to ensure it's skipped
- mergedPatterns.add(patternIdx)
+ // Mark every hashtag in this merged range so we don't render them as separate blocks
+ const mergeStartIndex = pattern.index
+ const mergeEndIndex = lineEndIndex
+ filteredPatterns.forEach((p, idx) => {
+ if (p.type === 'hashtag' && p.index >= mergeStartIndex && p.index < mergeEndIndex) {
+ const tag = p.data
+ hashtagsInContent.add(tag.toLowerCase())
+ mergedPatterns.add(idx)
+ }
+ })
}
} else if ((pattern.type === 'markdown-link' || pattern.type === 'relay-url') && (hasTextOnSameLine || hasTextBefore)) {
// Get the original pattern syntax from the content
@@ -1523,7 +1533,7 @@ function parseMarkdownContent(
normalizedText = normalizedText.replace(/[ \t]{2,}/g, ' ')
normalizedText = normalizedText.trim()
if (normalizedText) {
- const textContent = parseInlineMarkdown(normalizedText, `text-${patternIdx}-para-${paraIdx}-img-${imgIdx}`, footnotes)
+ const textContent = parseInlineMarkdown(normalizedText, `text-${patternIdx}-para-${paraIdx}-img-${imgIdx}`, footnotes, emojiInfos)
parts.push(
{textContent}
@@ -1586,7 +1596,7 @@ function parseMarkdownContent(
normalizedText = normalizedText.replace(/[ \t]{2,}/g, ' ')
normalizedText = normalizedText.trim()
if (normalizedText) {
- const textContent = parseInlineMarkdown(normalizedText, `text-${patternIdx}-para-${paraIdx}-final`, footnotes)
+ const textContent = parseInlineMarkdown(normalizedText, `text-${patternIdx}-para-${paraIdx}-final`, footnotes, emojiInfos)
parts.push(
{textContent}
@@ -1606,7 +1616,7 @@ function parseMarkdownContent(
normalizedPara = normalizedPara.trim()
if (normalizedPara) {
// Process paragraph for inline formatting (which will handle markdown links)
- const paraContent = parseInlineMarkdown(normalizedPara, `text-${patternIdx}-para-${paraIdx}`, footnotes)
+ const paraContent = parseInlineMarkdown(normalizedPara, `text-${patternIdx}-para-${paraIdx}`, footnotes, emojiInfos)
// Wrap in paragraph tag (no whitespace-pre-wrap, let normal text wrapping handle it)
parts.push(
@@ -1813,7 +1823,7 @@ function parseMarkdownContent(
} else if (pattern.type === 'markdown-link') {
const { text, url } = pattern.data
// Process the link text for inline formatting (bold, italic, etc.)
- const linkContent = parseInlineMarkdown(text, `link-${patternIdx}`, footnotes)
+ const linkContent = parseInlineMarkdown(text, `link-${patternIdx}`, footnotes, emojiInfos)
// Markdown links should always be rendered as inline links, not block-level components
// This ensures they don't break up the content flow when used in paragraphs
if (isWebsocketUrl(url)) {
@@ -1882,7 +1892,7 @@ function parseMarkdownContent(
} else if (pattern.type === 'header') {
const { level, text } = pattern.data
// Parse the header text for inline formatting (but not nested headers)
- const headerContent = parseInlineMarkdown(text, `header-${patternIdx}`, footnotes)
+ const headerContent = parseInlineMarkdown(text, `header-${patternIdx}`, footnotes, emojiInfos)
const HeaderTag = `h${Math.min(level, 6)}` as keyof JSX.IntrinsicElements
parts.push(
{listContent}
@@ -1913,7 +1923,7 @@ function parseMarkdownContent(
)
} else if (pattern.type === 'numbered-list-item') {
const { text, number } = pattern.data
- const listContent = parseInlineMarkdown(text, `numbered-${patternIdx}`, footnotes)
+ const listContent = parseInlineMarkdown(text, `numbered-${patternIdx}`, footnotes, emojiInfos)
const itemNumber = number ? parseInt(number, 10) : undefined
parts.push(
@@ -1935,7 +1945,7 @@ function parseMarkdownContent(
key={`th-${patternIdx}-${cellIdx}`}
className="border border-gray-300 dark:border-gray-700 px-4 py-2 bg-gray-100 dark:bg-gray-800 font-semibold text-left"
>
- {parseInlineMarkdown(cell, `table-header-${patternIdx}-${cellIdx}`, footnotes)}
+ {parseInlineMarkdown(cell, `table-header-${patternIdx}-${cellIdx}`, footnotes, emojiInfos)}
))}
@@ -1948,7 +1958,7 @@ function parseMarkdownContent(
key={`td-${patternIdx}-${rowIdx}-${cellIdx}`}
className="border border-gray-300 dark:border-gray-700 px-4 py-2"
>
- {parseInlineMarkdown(cell, `table-cell-${patternIdx}-${rowIdx}-${cellIdx}`, footnotes)}
+ {parseInlineMarkdown(cell, `table-cell-${patternIdx}-${rowIdx}-${cellIdx}`, footnotes, emojiInfos)}
))}
@@ -1987,7 +1997,7 @@ function parseMarkdownContent(
// Join paragraph lines with newlines to preserve line breaks (especially before em-dashes)
// This preserves the original formatting of the blockquote
const paragraphText = paragraphLines.join('\n')
- const paragraphContent = parseInlineMarkdown(paragraphText, `blockquote-${patternIdx}-para-${paraIdx}`, footnotes)
+ const paragraphContent = parseInlineMarkdown(paragraphText, `blockquote-${patternIdx}-para-${paraIdx}`, footnotes, emojiInfos)
return (
@@ -2010,7 +2020,7 @@ function parseMarkdownContent(
// Each line should have the > prefix preserved
const greentextContent = lines.map((line: string, lineIdx: number) => {
// Parse inline markdown for each line (for links, hashtags, etc.)
- const lineContent = parseInlineMarkdown(line, `greentext-${patternIdx}-line-${lineIdx}`, footnotes)
+ const lineContent = parseInlineMarkdown(line, `greentext-${patternIdx}-line-${lineIdx}`, footnotes, emojiInfos)
return (
{lineIdx > 0 && }
@@ -2256,7 +2266,7 @@ function parseMarkdownContent(
normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ')
normalizedPara = normalizedPara.trim()
if (normalizedPara) {
- const paraContent = parseInlineMarkdown(normalizedPara, `text-end-para-${imgIdx}-${paraIdx}`, footnotes)
+ const paraContent = parseInlineMarkdown(normalizedPara, `text-end-para-${imgIdx}-${paraIdx}`, footnotes, emojiInfos)
parts.push(
{paraContent}
@@ -2323,7 +2333,7 @@ function parseMarkdownContent(
normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ')
normalizedPara = normalizedPara.trim()
if (normalizedPara) {
- const paraContent = parseInlineMarkdown(normalizedPara, `text-end-final-para-${paraIdx}`, footnotes)
+ const paraContent = parseInlineMarkdown(normalizedPara, `text-end-final-para-${paraIdx}`, footnotes, emojiInfos)
parts.push(
{paraContent}
@@ -2342,7 +2352,7 @@ function parseMarkdownContent(
normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ')
normalizedPara = normalizedPara.trim()
if (normalizedPara) {
- const paraContent = parseInlineMarkdown(normalizedPara, `text-end-para-${paraIdx}`, footnotes)
+ const paraContent = parseInlineMarkdown(normalizedPara, `text-end-para-${paraIdx}`, footnotes, emojiInfos)
parts.push(
{paraContent}
@@ -2365,7 +2375,7 @@ function parseMarkdownContent(
normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ')
normalizedPara = normalizedPara.trim()
if (!normalizedPara) return null
- const paraContent = parseInlineMarkdown(normalizedPara, `text-only-para-${paraIdx}`, footnotes)
+ const paraContent = parseInlineMarkdown(normalizedPara, `text-only-para-${paraIdx}`, footnotes, emojiInfos)
return (
{paraContent}
@@ -2485,7 +2495,7 @@ function parseMarkdownContent(
const originalLine = listItemOriginalLines.get(patternIndex)
if (originalLine) {
// Render the original line with inline markdown processing
- const lineContent = parseInlineMarkdown(originalLine, `single-list-item-${partIdx}`, footnotes)
+ const lineContent = parseInlineMarkdown(originalLine, `single-list-item-${partIdx}`, footnotes, emojiInfos)
wrappedParts.push(
{lineContent}
@@ -2532,7 +2542,7 @@ function parseMarkdownContent(
className="text-sm text-gray-700 dark:text-gray-300"
>
[{id}]: {' '}
- {parseInlineMarkdown(text, `footnote-${id}`, footnotes)}
+ {parseInlineMarkdown(text, `footnote-${id}`, footnotes, emojiInfos)}
{' '}
= new Map()): React.ReactNode[] {
+function parseInlineMarkdown(text: string, keyPrefix: string, _footnotes: Map = new Map(), emojiInfos: TEmoji[] = []): React.ReactNode[] {
// Normalize newlines to spaces at the start (defensive - text should already be normalized, but ensure it)
// This prevents any hard breaks within inline content
text = text.replace(/\n/g, ' ')
@@ -2963,6 +2973,26 @@ function parseInlineMarkdown(text: string, keyPrefix: string, _footnotes: Map {
+ if (match.index !== undefined) {
+ const isInOther = inlinePatterns.some(p =>
+ (p.type === 'code' || p.type === 'bold' || p.type === 'italic' || p.type === 'strikethrough' || p.type === 'link' || p.type === 'hashtag' || p.type === 'relay-url' || p.type === 'nostr' || p.type === 'payto' || p.type === 'emoji') &&
+ match.index! >= p.index &&
+ match.index! < p.end
+ )
+ if (!isInOther) {
+ inlinePatterns.push({
+ index: match.index,
+ end: match.index + match[0].length,
+ type: 'emoji',
+ data: match[0].slice(1, -1).trim()
+ })
+ }
+ }
+ })
// Sort by index
inlinePatterns.sort((a, b) => a.index - b.index)
@@ -3015,11 +3045,11 @@ function parseInlineMarkdown(text: string, keyPrefix: string, _footnotes: Map
- {parseInlineMarkdown(text, `${keyPrefix}-link-${i}`, _footnotes)}
+ {parseInlineMarkdown(text, `${keyPrefix}-link-${i}`, _footnotes, emojiInfos)}
)
} else {
- const linkContent = parseInlineMarkdown(text, `${keyPrefix}-link-${i}`, _footnotes)
+ const linkContent = parseInlineMarkdown(text, `${keyPrefix}-link-${i}`, _footnotes, emojiInfos)
parts.push(
)
+ } else if (pattern.type === 'emoji') {
+ const shortcode = pattern.data as string
+ const custom = emojiInfos.find((e) => e.shortcode === shortcode)
+ if (custom) {
+ parts.push( )
+ } else {
+ const native = shortcodeToEmoji(shortcode, emojis) ?? shortcodeToEmoji(shortcode.replace(/\s+/g, '_'), emojis)
+ if (native?.emoji) {
+ parts.push( )
+ } else {
+ parts.push({`:${shortcode}:`} )
+ }
+ }
}
lastIndex = pattern.end
@@ -3457,6 +3500,8 @@ export default function MarkdownArticle({
return map
}, [event.id, JSON.stringify(event.tags), getImageIdentifier])
+ const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event.tags])
+
// Parse markdown content with post-processing for nostr: links and hashtags
const { nodes: parsedContent, hashtagsInContent } = useMemo(() => {
const result = parseMarkdownContent(preprocessedContent, {
@@ -3467,11 +3512,12 @@ export default function MarkdownArticle({
navigateToRelay,
videoPosterMap,
imageThumbnailMap,
- getImageIdentifier
+ getImageIdentifier,
+ emojiInfos
})
// Return nodes and hashtags (footnotes are already included in nodes)
return { nodes: result.nodes, hashtagsInContent: result.hashtagsInContent }
- }, [preprocessedContent, event.pubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay, videoPosterMap, imageThumbnailMap, getImageIdentifier])
+ }, [preprocessedContent, event.pubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay, videoPosterMap, imageThumbnailMap, getImageIdentifier, emojiInfos])
// Filter metadata tags to only show what's not already in content
const leftoverMetadataTags = useMemo(() => {
diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx
index a71527fe..b64d5cca 100644
--- a/src/components/NotificationList/index.tsx
+++ b/src/components/NotificationList/index.tsx
@@ -22,16 +22,21 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import PullToRefresh from 'react-simple-pull-to-refresh'
-import Tabs from '../Tabs'
import { NotificationItem } from './NotificationItem'
import { NotificationSkeleton } from './NotificationItem/Notification'
import { isTouchDevice } from '@/lib/utils'
-import { RefreshButton } from '../RefreshButton'
-
const LIMIT = 100
const SHOW_COUNT = 30
-const NotificationList = forwardRef((_, ref) => {
+const NotificationList = forwardRef(
+ (
+ {
+ notificationType
+ }: {
+ notificationType: TNotificationType
+ },
+ ref
+ ) => {
const { t } = useTranslation()
const { current, display } = usePrimaryPage()
const active = useMemo(() => current === 'notifications' && display, [current, display])
@@ -39,7 +44,6 @@ const NotificationList = forwardRef((_, ref) => {
const { getNotificationsSeenAt } = useNotification()
const { notificationListStyle } = useUserPreferences()
const { favoriteRelays } = useFavoriteRelays()
- const [notificationType, setNotificationType] = useState('all')
const [lastReadTime, setLastReadTime] = useState(0)
const [refreshCount, setRefreshCount] = useState(0)
const [timelineKey, setTimelineKey] = useState(undefined)
@@ -92,17 +96,10 @@ const NotificationList = forwardRef((_, ref) => {
[loading]
)
- // Listen for tab restoration from PageManager
+ // Reset visible count when tab changes (parent owns tab state)
useEffect(() => {
- const handleRestore = (e: CustomEvent<{ page: string, tab: string }>) => {
- if (e.detail.page === 'notifications' && e.detail.tab) {
- setNotificationType(e.detail.tab as TNotificationType)
- setShowCount(SHOW_COUNT)
- }
- }
- window.addEventListener('restorePageTab', handleRestore as EventListener)
- return () => window.removeEventListener('restorePageTab', handleRestore as EventListener)
- }, [])
+ setShowCount(SHOW_COUNT)
+ }, [notificationType])
const handleNewEvent = useCallback(
(event: NostrEvent) => {
@@ -318,25 +315,7 @@ const NotificationList = forwardRef((_, ref) => {
return (
-
{
- setShowCount(SHOW_COUNT)
- setNotificationType(type as TNotificationType)
- // Dispatch tab change event for PageManager
- window.dispatchEvent(new CustomEvent('pageTabChanged', {
- detail: { page: 'notifications', tab: type }
- }))
- }}
- options={!supportTouch ? refresh()} /> : null}
- />
-
+
{supportTouch ? (
{
@@ -352,6 +331,7 @@ const NotificationList = forwardRef((_, ref) => {
)}
)
-})
+ }
+)
NotificationList.displayName = 'NotificationList'
export default NotificationList
diff --git a/src/constants.ts b/src/constants.ts
index 06ae7802..8fba58df 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -228,7 +228,8 @@ export const URL_REGEX =
export const WS_URL_REGEX =
/wss?:\/\/[\w\p{L}\p{N}\p{M}&.\-/?=#@%+_:!~*]+[^\s.,;:'")\]}!?,。;:"'!?】)]/giu
export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
-export const EMOJI_SHORT_CODE_REGEX = /:[a-zA-Z0-9_-]+:/g
+/** Matches :shortcode: or :short code: (allows letters, digits, underscore, hyphen, space) */
+export const EMOJI_SHORT_CODE_REGEX = /:[a-zA-Z0-9_\-\s]+:/g
export const EMBEDDED_EVENT_REGEX = /nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g
export const EMBEDDED_MENTION_REGEX = /nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+)/g
export const HASHTAG_REGEX = /#[a-zA-Z0-9_\-\u00C0-\u017F\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF]+/g
diff --git a/src/lib/nostr-parser.tsx b/src/lib/nostr-parser.tsx
index 422eb1fa..50e5e01c 100644
--- a/src/lib/nostr-parser.tsx
+++ b/src/lib/nostr-parser.tsx
@@ -261,7 +261,9 @@ export function parseNostrContent(content: string, event?: Event): ParsedNostrCo
if (type === 'nostr') {
const bech32Id = match[1]
const nostrType = getNostrType(bech32Id)
-
+ // Store content without leading whitespace/newline (regex may capture \n or space before "nostr:")
+ const nostrContent = `nostr:${bech32Id}`
+
// Add spacing around handles if they're not at the beginning or end of a line
const isAtStart = start === 0 || content[start - 1] === '\n'
const isAtEnd = end === content.length || content[end] === '\n'
@@ -277,7 +279,7 @@ export function parseNostrContent(content: string, event?: Event): ParsedNostrCo
elements.push({
type: 'nostr',
- content: match[0],
+ content: nostrContent,
bech32Id,
nostrType: nostrType || undefined
})
@@ -288,7 +290,15 @@ export function parseNostrContent(content: string, event?: Event): ParsedNostrCo
content: ' '
})
}
-
+
+ // If nostr is at the start of a line and immediately followed by a single newline, skip it
+ // so we don't render an errant hard-return behind the address
+ if (isAtStart && content[end] === '\n') {
+ lastIndex = end + 1
+ } else {
+ lastIndex = end
+ }
+ continue
} else if (['image', 'video', 'audio'].includes(type) && url) {
elements.push({
type: type as 'image' | 'video' | 'audio',
diff --git a/src/pages/primary/DiscussionsPage/ThreadCard.tsx b/src/pages/primary/DiscussionsPage/ThreadCard.tsx
index a7902aa2..36a40a70 100644
--- a/src/pages/primary/DiscussionsPage/ThreadCard.tsx
+++ b/src/pages/primary/DiscussionsPage/ThreadCard.tsx
@@ -2,7 +2,10 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Clock, Hash, Users } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
-import { formatDistanceToNow } from 'date-fns'
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+
+dayjs.extend(relativeTime)
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { DISCUSSION_TOPICS } from './discussionTopics'
@@ -54,14 +57,13 @@ export default function ThreadCard({
// Get all topics from this thread
const allTopics = extractAllTopics(thread)
- // Format creation time
- const createdAt = new Date(thread.created_at * 1000)
- const timeAgo = formatDistanceToNow(createdAt, { addSuffix: true })
-
+ // Format creation time (fromNow() includes suffix e.g. "3 hours ago")
+ const timeAgo = dayjs.unix(thread.created_at).fromNow()
+
// Format last activity times
const formatLastActivity = (timestamp: number) => {
if (timestamp === 0) return null
- return formatDistanceToNow(new Date(timestamp * 1000), { addSuffix: true })
+ return dayjs.unix(timestamp).fromNow()
}
const lastCommentAgo = formatLastActivity(lastCommentTime)
diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx
index 33571ce8..5cce8a57 100644
--- a/src/pages/primary/DiscussionsPage/index.tsx
+++ b/src/pages/primary/DiscussionsPage/index.tsx
@@ -1042,7 +1042,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
titlebar={ }
displayScrollToTopButton
>
-
+
setShowCreateDialog(true)}
diff --git a/src/pages/primary/MePage/index.tsx b/src/pages/primary/MePage/index.tsx
index e4ba3d68..827f0c2b 100644
--- a/src/pages/primary/MePage/index.tsx
+++ b/src/pages/primary/MePage/index.tsx
@@ -39,7 +39,7 @@ const MePage = forwardRef((_, ref) => {
titlebar={ }
hideTitlebarBottomBorder
>
-
+
@@ -53,6 +53,7 @@ const MePage = forwardRef((_, ref) => {
titlebar={
}
hideTitlebarBottomBorder
>
+
@@ -94,6 +95,7 @@ const MePage = forwardRef((_, ref) => {
+
)
})
diff --git a/src/pages/primary/NoteListPage/RelaysFeed.tsx b/src/pages/primary/NoteListPage/RelaysFeed.tsx
index 7a3493f8..89c6e49a 100644
--- a/src/pages/primary/NoteListPage/RelaysFeed.tsx
+++ b/src/pages/primary/NoteListPage/RelaysFeed.tsx
@@ -3,9 +3,13 @@ import { checkAlgoRelay } from '@/lib/relay'
import logger from '@/lib/logger'
import { useFeed } from '@/providers/FeedProvider'
import relayInfoService from '@/services/relay-info.service'
-import { useEffect, useMemo, useState } from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
-export default function RelaysFeed() {
+export default function RelaysFeed({
+ setSubHeader
+}: {
+ setSubHeader?: (node: React.ReactNode) => void
+}) {
logger.debug('RelaysFeed component rendering')
const { feedInfo, relayUrls } = useFeed()
const [isReady, setIsReady] = useState(false)
@@ -55,6 +59,7 @@ export default function RelaysFeed() {
areAlgoRelays={areAlgoRelays}
isMainFeed
showRelayCloseReason
+ setSubHeader={setSubHeader}
/>
)
}
diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx
index 08c1c013..b8e181f4 100644
--- a/src/pages/primary/NoteListPage/index.tsx
+++ b/src/pages/primary/NoteListPage/index.tsx
@@ -9,7 +9,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { TPageRef } from '@/types'
import { Info, Rss } from 'lucide-react'
-import {
+import React, {
Dispatch,
forwardRef,
SetStateAction,
@@ -36,8 +36,16 @@ const NoteListPage = forwardRef((_, ref) => {
const { pubkey, checkLogin } = useNostr()
const { feedInfo, relayUrls, isReady } = useFeed()
const [showRelayDetails, setShowRelayDetails] = useState(false)
+ const [homeSubHeader, setHomeSubHeader] = useState
(null)
useImperativeHandle(ref, () => layoutRef.current)
+ // Clear subHeader when switching away from relay/relays/all-favorites feed
+ useEffect(() => {
+ const isRelaysFeed =
+ feedInfo.feedType === 'relay' || feedInfo.feedType === 'relays' || feedInfo.feedType === 'all-favorites'
+ if (!isRelaysFeed) setHomeSubHeader(null)
+ }, [feedInfo.feedType])
+
// REMOVED: Scroll-to-top logic - feed should NEVER scroll to top when drawer opens/closes
// The feed stays mounted and maintains scroll position at all times
@@ -89,7 +97,7 @@ const NoteListPage = forwardRef((_, ref) => {
{showRelayDetails && feedInfo.feedType === 'relay' && !!feedInfo.id && (
)}
-
+
>
)
}
@@ -107,10 +115,13 @@ const NoteListPage = forwardRef((_, ref) => {
}
/>
}
+ subHeader={homeSubHeader ?? undefined}
displayScrollToTopButton
>
-
- {content}
+
+
+ {content}
+
)
})
diff --git a/src/pages/primary/NotificationListPage/index.tsx b/src/pages/primary/NotificationListPage/index.tsx
index b30b70e7..aab3579f 100644
--- a/src/pages/primary/NotificationListPage/index.tsx
+++ b/src/pages/primary/NotificationListPage/index.tsx
@@ -1,15 +1,22 @@
import HideUntrustedContentButton from '@/components/HideUntrustedContentButton'
import NotificationList from '@/components/NotificationList'
-import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
+import { RefreshButton } from '@/components/RefreshButton'
+import Tabs from '@/components/Tabs'
import { usePrimaryPage } from '@/PageManager'
+import { TNotificationType } from '@/types'
+import { isTouchDevice } from '@/lib/utils'
+import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { Bell } from 'lucide-react'
-import { forwardRef, useEffect, useRef } from 'react'
+import { forwardRef, useEffect, useRef, useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
const NotificationListPage = forwardRef((_, ref) => {
+ const { t } = useTranslation()
const { current } = usePrimaryPage()
const firstRenderRef = useRef(true)
const notificationListRef = useRef<{ refresh: () => void }>(null)
+ const [notificationType, setNotificationType] = useState('all')
+ const supportTouch = useMemo(() => isTouchDevice(), [])
useEffect(() => {
if (current === 'notifications' && !firstRenderRef.current) {
@@ -18,14 +25,47 @@ const NotificationListPage = forwardRef((_, ref) => {
firstRenderRef.current = false
}, [current])
+ useEffect(() => {
+ const handleRestore = (e: CustomEvent<{ page: string; tab: string }>) => {
+ if (e.detail.page === 'notifications' && e.detail.tab) {
+ setNotificationType(e.detail.tab as TNotificationType)
+ }
+ }
+ window.addEventListener('restorePageTab', handleRestore as EventListener)
+ return () => window.removeEventListener('restorePageTab', handleRestore as EventListener)
+ }, [])
+
return (
}
+ subHeader={
+ {
+ setNotificationType(tab as TNotificationType)
+ window.dispatchEvent(new CustomEvent('pageTabChanged', {
+ detail: { page: 'notifications', tab }
+ }))
+ }}
+ options={!supportTouch ? notificationListRef.current?.refresh()} /> : null}
+ />
+ }
displayScrollToTopButton
>
-
+
+
+
)
})
diff --git a/src/pages/primary/ProfilePage/index.tsx b/src/pages/primary/ProfilePage/index.tsx
index 7b4646c8..9302a91f 100644
--- a/src/pages/primary/ProfilePage/index.tsx
+++ b/src/pages/primary/ProfilePage/index.tsx
@@ -15,7 +15,9 @@ const ProfilePage = forwardRef((_, ref) => {
displayScrollToTopButton
ref={ref}
>
-
+
)
})
diff --git a/src/pages/primary/RelayPage/index.tsx b/src/pages/primary/RelayPage/index.tsx
index 523e7da5..cbc9ce07 100644
--- a/src/pages/primary/RelayPage/index.tsx
+++ b/src/pages/primary/RelayPage/index.tsx
@@ -14,7 +14,9 @@ const RelayPage = forwardRef(({ url }: { url?: string }, ref) => {
displayScrollToTopButton
ref={ref}
>
-
+
+
+
)
})
diff --git a/src/pages/primary/SearchPage/index.tsx b/src/pages/primary/SearchPage/index.tsx
index 21706c81..a97f2514 100644
--- a/src/pages/primary/SearchPage/index.tsx
+++ b/src/pages/primary/SearchPage/index.tsx
@@ -44,7 +44,7 @@ const SearchPage = forwardRef((_, ref) => {
titlebar={null}
displayScrollToTopButton
>
-