diff --git a/src/App.tsx b/src/App.tsx
index 56b6059..f819b4f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,7 @@ import { ContentPolicyProvider } from '@/providers/ContentPolicyProvider'
import { DeletedEventProvider } from '@/providers/DeletedEventProvider'
import { FavoriteRelaysProvider } from '@/providers/FavoriteRelaysProvider'
import { FeedProvider } from '@/providers/FeedProvider'
+import { FontSizeProvider } from '@/providers/FontSizeProvider'
import { FollowListProvider } from '@/providers/FollowListProvider'
import { GroupListProvider } from '@/providers/GroupListProvider'
import { InterestListProvider } from '@/providers/InterestListProvider'
@@ -26,8 +27,9 @@ import { PageManager } from './PageManager'
export default function App(): JSX.Element {
return (
-
-
+
+
+
@@ -64,6 +66,7 @@ export default function App(): JSX.Element {
+
)
}
diff --git a/src/components/ArticleExportMenu/ArticleExportMenu.tsx b/src/components/ArticleExportMenu/ArticleExportMenu.tsx
index f8fc3de..cfa83d3 100644
--- a/src/components/ArticleExportMenu/ArticleExportMenu.tsx
+++ b/src/components/ArticleExportMenu/ArticleExportMenu.tsx
@@ -7,7 +7,8 @@ import {
} from '@/components/ui/dropdown-menu'
import { MoreVertical, FileDown } from 'lucide-react'
import logger from '@/lib/logger'
-import { Event } from 'nostr-tools'
+import { Event, kinds } from 'nostr-tools'
+import { ExtendedKind } from '@/constants'
interface ArticleExportMenuProps {
event: Event
@@ -15,13 +16,23 @@ interface ArticleExportMenuProps {
}
export default function ArticleExportMenu({ event, title }: ArticleExportMenuProps) {
+ // Determine export format based on event kind
+ const getExportFormat = () => {
+ if (event.kind === kinds.LongFormArticle || event.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) {
+ return { extension: 'md', mimeType: 'text/markdown', label: 'Markdown' }
+ }
+ // For 30818, 30041, 30040 - use AsciiDoc
+ return { extension: 'adoc', mimeType: 'text/plain', label: 'AsciiDoc' }
+ }
+
const exportArticle = async () => {
try {
const content = event.content
- const filename = `${title}.adoc`
+ const format = getExportFormat()
+ const filename = `${title}.${format.extension}`
- // Export raw AsciiDoc content
- const blob = new Blob([content], { type: 'text/plain' })
+ // Export raw content
+ const blob = new Blob([content], { type: format.mimeType })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
@@ -32,24 +43,26 @@ export default function ArticleExportMenu({ event, title }: ArticleExportMenuPro
document.body.removeChild(a)
URL.revokeObjectURL(url)
- logger.info('[ArticleExportMenu] Exported article as .adoc')
+ logger.info(`[ArticleExportMenu] Exported article as .${format.extension}`)
} catch (error) {
logger.error('[ArticleExportMenu] Error exporting article:', error)
alert('Failed to export article. Please try again.')
}
}
+ const format = getExportFormat()
+
return (
e.stopPropagation()}>
-
e.stopPropagation()}>
- Export as AsciiDoc
+ Export as {format.label}
diff --git a/src/components/Note/DiscussionContent/index.tsx b/src/components/Note/DiscussionContent/index.tsx
deleted file mode 100644
index 32b8e4c..0000000
--- a/src/components/Note/DiscussionContent/index.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Event } from 'nostr-tools'
-import { useMemo } from 'react'
-import { parseNostrContent, renderNostrContent } from '@/lib/nostr-parser.tsx'
-import { cn } from '@/lib/utils'
-
-export default function DiscussionContent({
- event,
- className
-}: {
- event: Event
- className?: string
-}) {
- const parsedContent = useMemo(() => {
- return parseNostrContent(event.content, event)
- }, [event.content, event])
-
- return renderNostrContent(parsedContent, cn('prose prose-sm prose-zinc max-w-none break-words dark:prose-invert w-full', className))
-}
diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
index 7e80c7c..8f3acce 100644
--- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
+++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
@@ -402,6 +402,7 @@ export default function MarkdownArticle({
.hljs {
background: transparent !important;
}
+ /* Light theme syntax highlighting */
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
@@ -411,7 +412,7 @@ export default function MarkdownArticle({
.hljs-type,
.hljs-name,
.hljs-strong {
- color: #f85149 !important;
+ color: #dc2626 !important; /* red-600 - good contrast on light */
font-weight: bold !important;
}
.hljs-string,
@@ -426,40 +427,100 @@ export default function MarkdownArticle({
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
- color: #0366d6 !important;
+ color: #0284c7 !important; /* sky-600 */
}
.hljs-comment,
.hljs-quote {
- color: #8b949e !important;
+ color: #6b7280 !important; /* gray-500 */
}
.hljs-number,
.hljs-deletion {
- color: #005cc5 !important;
+ color: #0d9488 !important; /* teal-600 */
}
.hljs-variable,
.hljs-template-variable,
.hljs-link {
- color: #e36209 !important;
+ color: #ea580c !important; /* orange-600 */
}
.hljs-meta {
- color: #6f42c1 !important;
+ color: #7c3aed !important; /* violet-600 */
}
.hljs-built_in,
.hljs-class .hljs-title {
- color: #005cc5 !important;
+ color: #0d9488 !important; /* teal-600 */
}
.hljs-params {
- color: #f0f6fc !important;
+ color: #1f2937 !important; /* gray-800 */
}
.hljs-attribute {
- color: #005cc5 !important;
+ color: #0d9488 !important; /* teal-600 */
}
.hljs-function .hljs-title {
- color: #6f42c1 !important;
+ color: #7c3aed !important; /* violet-600 */
}
.hljs-subst {
- color: #f0f6fc !important;
+ color: #1f2937 !important; /* gray-800 */
}
+
+ /* Dark theme syntax highlighting */
+ .dark .hljs-keyword,
+ .dark .hljs-selector-tag,
+ .dark .hljs-literal,
+ .dark .hljs-title,
+ .dark .hljs-section,
+ .dark .hljs-doctag,
+ .dark .hljs-type,
+ .dark .hljs-name,
+ .dark .hljs-strong {
+ color: #f87171 !important; /* red-400 */
+ }
+ .dark .hljs-string,
+ .dark .hljs-title.class_,
+ .dark .hljs-attr,
+ .dark .hljs-symbol,
+ .dark .hljs-bullet,
+ .dark .hljs-addition,
+ .dark .hljs-code,
+ .dark .hljs-regexp,
+ .dark .hljs-selector-pseudo,
+ .dark .hljs-selector-attr,
+ .dark .hljs-selector-class,
+ .dark .hljs-selector-id {
+ color: #38bdf8 !important; /* sky-400 */
+ }
+ .dark .hljs-comment,
+ .dark .hljs-quote {
+ color: #9ca3af !important; /* gray-400 */
+ }
+ .dark .hljs-number,
+ .dark .hljs-deletion {
+ color: #5eead4 !important; /* teal-300 */
+ }
+ .dark .hljs-variable,
+ .dark .hljs-template-variable,
+ .dark .hljs-link {
+ color: #fb923c !important; /* orange-400 */
+ }
+ .dark .hljs-meta {
+ color: #a78bfa !important; /* violet-400 */
+ }
+ .dark .hljs-built_in,
+ .dark .hljs-class .hljs-title {
+ color: #5eead4 !important; /* teal-300 */
+ }
+ .dark .hljs-params {
+ color: #e5e7eb !important; /* gray-200 */
+ }
+ .dark .hljs-attribute {
+ color: #5eead4 !important; /* teal-300 */
+ }
+ .dark .hljs-function .hljs-title {
+ color: #a78bfa !important; /* violet-400 */
+ }
+ .dark .hljs-subst {
+ color: #e5e7eb !important; /* gray-200 */
+ }
+
.hljs-emphasis {
font-style: italic;
}
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index de01511..cfe1e6a 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -20,7 +20,6 @@ import UserAvatar from '../UserAvatar'
import Username from '../Username'
import { MessageSquare } from 'lucide-react'
import CommunityDefinition from './CommunityDefinition'
-import DiscussionContent from './DiscussionContent'
import GroupMetadata from './GroupMetadata'
import Highlight from './Highlight'
@@ -142,7 +141,7 @@ export default function Note({
content = (
<>
{title}
-
+
>
)
} else if (event.kind === ExtendedKind.POLL) {
@@ -183,7 +182,7 @@ export default function Note({
onClick={(e) => {
// Don't navigate if clicking on interactive elements
const target = e.target as HTMLElement
- if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-embedded-note]') || target.closest('[data-parent-note-preview]')) {
+ if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-embedded-note]') || target.closest('[data-parent-note-preview]') || target.closest('[data-user-avatar]') || target.closest('[data-username]')) {
return
}
navigateToNote(toNote(event))
diff --git a/src/components/ParentNotePreview/index.tsx b/src/components/ParentNotePreview/index.tsx
index d467199..1c19aed 100644
--- a/src/components/ParentNotePreview/index.tsx
+++ b/src/components/ParentNotePreview/index.tsx
@@ -1,7 +1,11 @@
import { Skeleton } from '@/components/ui/skeleton'
+import { SEARCHABLE_RELAY_URLS } from '@/constants'
import { useFetchEvent } from '@/hooks'
import { cn } from '@/lib/utils'
+import client from '@/services/client.service'
import { useTranslation } from 'react-i18next'
+import { useCallback, useEffect, useState } from 'react'
+import { Event, nip19 } from 'nostr-tools'
import ContentPreview from '../ContentPreview'
import UserAvatar from '../UserAvatar'
@@ -16,8 +20,58 @@ export default function ParentNotePreview({
}) {
const { t } = useTranslation()
const { event, isFetching } = useFetchEvent(eventId)
+ const [fallbackEvent, setFallbackEvent] = useState(undefined)
+ const [isFetchingFallback, setIsFetchingFallback] = useState(false)
- if (isFetching) {
+ // Helper function to decode event ID
+ const getHexEventId = (id: string): string | null => {
+ if (/^[0-9a-f]{64}$/.test(id)) {
+ return id
+ }
+ try {
+ const { type, data } = nip19.decode(id)
+ if (type === 'note') {
+ return data
+ } else if (type === 'nevent') {
+ return data.id
+ }
+ // Can't fetch naddr with fetchEventWithExternalRelays
+ return null
+ } catch (err) {
+ // Invalid bech32 or already hex
+ return null
+ }
+ }
+
+ // Helper function to fetch from searchable relays
+ const fetchFromSearchableRelays = useCallback(async () => {
+ const hexEventId = getHexEventId(eventId)
+ if (!hexEventId) return
+
+ setIsFetchingFallback(true)
+ try {
+ const foundEvent = await client.fetchEventWithExternalRelays(hexEventId, SEARCHABLE_RELAY_URLS)
+ if (foundEvent) {
+ setFallbackEvent(foundEvent)
+ }
+ } catch (error) {
+ console.warn('Fallback fetch from searchable relays failed:', error)
+ } finally {
+ setIsFetchingFallback(false)
+ }
+ }, [eventId])
+
+ // If the initial fetch fails, try fetching from searchable relays automatically
+ useEffect(() => {
+ if (!isFetching && !event && !fallbackEvent && !isFetchingFallback && eventId) {
+ fetchFromSearchableRelays()
+ }
+ }, [isFetching, event, eventId, fallbackEvent, isFetchingFallback, fetchFromSearchableRelays])
+
+ const finalEvent = event || fallbackEvent
+ const finalIsFetching = isFetching || isFetchingFallback
+
+ if (finalIsFetching) {
return (
) => {
+ if (finalEvent) {
+ onClick?.(e)
+ } else if (!finalEvent && !finalIsFetching && eventId) {
+ // Retry fetch from searchable relays when clicking "Note not found"
+ e.stopPropagation()
+ fetchFromSearchableRelays()
+ }
+ }
+
return (
{t('reply to')}
- {event &&
}
-
+ {finalEvent &&
}
+
+
+
)
}
diff --git a/src/components/Tabs/index.tsx b/src/components/Tabs/index.tsx
index 8ca6cde..177cb77 100644
--- a/src/components/Tabs/index.tsx
+++ b/src/components/Tabs/index.tsx
@@ -111,14 +111,14 @@ export default function Tabs({
deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : ''
)}
>
-
-
+
+
{tabs.map((tab, index) => (
(tabRefs.current[index] = el)}
className={cn(
- `w-fit text-center py-2 px-6 my-1 font-semibold whitespace-nowrap clickable cursor-pointer rounded-lg`,
+ `w-fit text-center py-2 px-6 my-1 font-semibold whitespace-nowrap clickable cursor-pointer rounded-lg shrink-0`,
value === tab.value ? '' : 'text-muted-foreground'
)}
onClick={() => {
diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx
index 6176ba8..7df4949 100644
--- a/src/components/UserAvatar/index.tsx
+++ b/src/components/UserAvatar/index.tsx
@@ -44,8 +44,12 @@ export default function UserAvatar({
return (
navigateToProfile(toProfile(pubkey))}
+ onClick={(e) => {
+ e.stopPropagation()
+ navigateToProfile(toProfile(pubkey))
+ }}
>
diff --git a/src/components/Username/index.tsx b/src/components/Username/index.tsx
index 76abc34..d852b03 100644
--- a/src/components/Username/index.tsx
+++ b/src/components/Username/index.tsx
@@ -38,9 +38,13 @@ export default function Username({
return (
navigateToProfile(toProfile(pubkey))}
+ onClick={(e) => {
+ e.stopPropagation()
+ navigateToProfile(toProfile(pubkey))
+ }}
>
{showAt && '@'}
{username}
diff --git a/src/constants.ts b/src/constants.ts
index 1d901c8..81728c4 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -19,6 +19,7 @@ export const RECOMMENDED_BLOSSOM_SERVERS = [
export const StorageKey = {
VERSION: 'version',
THEME_SETTING: 'themeSetting',
+ FONT_SIZE: 'fontSize',
RELAY_SETS: 'relaySets',
ACCOUNTS: 'accounts',
CURRENT_ACCOUNT: 'currentAccount',
@@ -63,6 +64,12 @@ export const StorageKey = {
FEED_TYPE: 'feedType' // deprecated
}
+export const FONT_SIZE = {
+ SMALL: 'small',
+ MEDIUM: 'medium',
+ LARGE: 'large'
+} as const
+
export const ApplicationDataKey = {
NOTIFICATIONS_SEEN_AT: 'seen_notifications_at'
}
diff --git a/src/index.css b/src/index.css
index cd8d258..cee049b 100644
--- a/src/index.css
+++ b/src/index.css
@@ -91,12 +91,12 @@
--secondary: 240 4.8% 94%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 94%;
- --muted-foreground: 240 3.8% 46.1%;
+ --muted-foreground: 240 5% 35%;
--accent: 240 4.8% 94%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
- --border: 240 5.9% 90%;
+ --border: 240 5.9% 85%;
--input: 240 5.9% 90%;
--ring: 140 70% 28%;
--chart-1: 12 76% 61%;
@@ -120,12 +120,12 @@
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
- --muted-foreground: 240 5% 64.9%;
+ --muted-foreground: 240 5% 75%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
- --border: 240 3.7% 15.9%;
+ --border: 240 3.7% 25%;
--input: 240 3.7% 15.9%;
--ring: 140 70% 40%;
--chart-1: 220 70% 50%;
@@ -138,6 +138,46 @@
.dark input[type='datetime-local']::-webkit-calendar-picker-indicator {
filter: invert(1) brightness(1.5);
}
+
+ /* Focus indicators for accessibility */
+ *:focus-visible {
+ outline: 2px solid hsl(var(--ring));
+ outline-offset: 2px;
+ border-radius: 2px;
+ }
+
+ /* Ensure proper contrast for interactive elements */
+ button:not(:disabled),
+ a,
+ [role="button"] {
+ transition: opacity 0.2s;
+ }
+
+ button:disabled,
+ [aria-disabled="true"] {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+/* Font Size Adjustments */
+.font-size-small {
+ font-size: 87.5% !important; /* 14px base */
+}
+
+.font-size-medium {
+ font-size: 100% !important; /* 16px base */
+}
+
+.font-size-large {
+ font-size: 112.5% !important; /* 18px base */
+}
+
+/* Apply font size to content areas */
+.font-size-small .prose,
+.font-size-medium .prose,
+.font-size-large .prose {
+ font-size: var(--content-font-size, 1rem);
}
/* AsciiDoc Table of Contents Styling */
diff --git a/src/lib/discussion-topics.ts b/src/lib/discussion-topics.ts
index cee51bd..911e330 100644
--- a/src/lib/discussion-topics.ts
+++ b/src/lib/discussion-topics.ts
@@ -276,19 +276,24 @@ export function buildGroupDisplayName(
groupId: string,
groupRelay: string | null
): string {
+ let displayName: string
+
if (!groupRelay) {
- return groupId
+ displayName = groupId
+ } else {
+ // Extract hostname from relay URL for cleaner display
+ try {
+ const url = new URL(groupRelay)
+ const hostname = url.hostname
+ displayName = `${hostname}'${groupId}`
+ } catch {
+ // Fallback to full relay URL if parsing fails
+ displayName = `${groupRelay}'${groupId}`
+ }
}
- // Extract hostname from relay URL for cleaner display
- try {
- const url = new URL(groupRelay)
- const hostname = url.hostname
- return `${hostname}'${groupId}`
- } catch {
- // Fallback to full relay URL if parsing fails
- return `${groupRelay}'${groupId}`
- }
+ // Truncate to 20 characters
+ return displayName.length > 20 ? displayName.substring(0, 20) : displayName
}
/**
diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
index a1be204..8297675 100644
--- a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
+++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
@@ -24,7 +24,7 @@ import { simplifyUrl } from '@/lib/url'
import relaySelectionService from '@/services/relay-selection.service'
import dayjs from 'dayjs'
import { extractHashtagsFromContent, normalizeTopic } from '@/lib/discussion-topics'
-import DiscussionContent from '@/components/Note/DiscussionContent'
+import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import RelayIcon from '@/components/RelayIcon'
import logger from '@/lib/logger'
@@ -711,25 +711,24 @@ export default function CreateThreadDialog({
{/* Preview of the content */}
-
-
-
+
) : (
diff --git a/src/pages/primary/DiscussionsPage/ThreadCard.tsx b/src/pages/primary/DiscussionsPage/ThreadCard.tsx
index 0c6a689..ad02328 100644
--- a/src/pages/primary/DiscussionsPage/ThreadCard.tsx
+++ b/src/pages/primary/DiscussionsPage/ThreadCard.tsx
@@ -93,17 +93,11 @@ export default function ThreadCard({
{title}
-
+
{topicInfo.id}
- {groupInfo.isGroupDiscussion && groupInfo.groupDisplayName && (
-
-
- {groupInfo.groupDisplayName}
-
- )}
{allTopics.slice(0, 3).map(topic => (
@@ -111,6 +105,14 @@ export default function ThreadCard({
))}
+ {groupInfo.isGroupDiscussion && groupInfo.groupDisplayName && (
+
+
+
+ {groupInfo.groupDisplayName}
+
+
+ )}
@@ -146,17 +148,11 @@ export default function ThreadCard({
{title}
-
+
{topicInfo.label}
- {groupInfo.isGroupDiscussion && groupInfo.groupDisplayName && (
-
-
- {groupInfo.groupDisplayName}
-
- )}
{allTopics.slice(0, 3).map(topic => (
@@ -173,6 +169,14 @@ export default function ThreadCard({
{t('last updated')}: {lastCommentAgo || lastVoteAgo || timeAgo}
+ {groupInfo.isGroupDiscussion && groupInfo.groupDisplayName && (
+
+
+
+ {groupInfo.groupDisplayName}
+
+
+ )}
diff --git a/src/pages/secondary/GeneralSettingsPage/index.tsx b/src/pages/secondary/GeneralSettingsPage/index.tsx
index b6e88ce..b1857b1 100644
--- a/src/pages/secondary/GeneralSettingsPage/index.tsx
+++ b/src/pages/secondary/GeneralSettingsPage/index.tsx
@@ -1,11 +1,12 @@
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
-import { MEDIA_AUTO_LOAD_POLICY, NOTIFICATION_LIST_STYLE } from '@/constants'
+import { FONT_SIZE, MEDIA_AUTO_LOAD_POLICY, NOTIFICATION_LIST_STYLE } from '@/constants'
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { cn, isSupportCheckConnectionType } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
+import { useFontSize } from '@/providers/FontSizeProvider'
import { useTheme } from '@/providers/ThemeProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
@@ -19,6 +20,7 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
const { t, i18n } = useTranslation()
const [language, setLanguage] = useState
(i18n.language as TLanguage)
const { themeSetting, setThemeSetting } = useTheme()
+ const { fontSize, setFontSize } = useFontSize()
const {
autoplay,
setAutoplay,
@@ -72,6 +74,21 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
+
+
+
+