KindFeedPage's own useLayoutOptions was overwriting the onFabClick
set by WebxdcFeedPage. Now onFabClick is a KindFeedPage prop that
gets forwarded to useLayoutOptions, so only one call wins.
Replace the custom WebxdcFeedPage with KindFeedPage, powered by
a new tagFilters option on useFeed/Feed/KindFeedPage. The webxdc
page now uses the same follows/global tabs, pull-to-refresh, and
NoteCard rendering as every other feed.
- useFeed: accept optional tagFilters (e.g. { '#m': [...] })
- Feed/KindFeedPage: pass tagFilters through
- FloatingComposeButton: accept onFabClick callback override
- LayoutOptions: add onFabClick for custom FAB behavior
- Remove useWebxdcFeed (no longer needed)
Description now appears in its own rounded bg-secondary/50 card
below the media element, similar to YouTube's description box.
Media renders unwrapped at full width.
Each MIME type now renders inside a rounded border card. The media
is the hero at the top and the description sits in a separated
footer section below. When there's no description, images and
videos skip the card wrapper to stay clean.
Media (webxdc embed, image, video, audio) is now the hero element
rendered first. Descriptions appear below as captions. Audio gets
a styled card with track name, file size, and download button.
Generic files get a polished card with MIME label and size info.
Kind 1063 file metadata events now dispatch to FileMetadataContent
which renders based on MIME type: webxdc apps get WebxdcEmbed,
images get ImageGallery, videos get VideoPlayer, audio gets a
native player, and unknown types show a download link.
Upload a .xdc file, extract manifest metadata (name, icon),
add a description, and publish a NIP-94 kind 1063 file metadata
event with url, m, x, alt, and webxdc tags. The FAB on the
/webxdc page opens this dialog instead of the kind-1 composer.
The compose dialog already handles .xdc file uploads with full
webxdc metadata extraction (name, icon, UUID), so the FAB uses
fabKind: 1 to open the standard compose flow.
Query NIP-94 kind 1063 events filtered by #m tag for
application/x-webxdc MIME type. Includes infinite scroll,
pull-to-refresh, embedded webxdc launcher, and onboarding
content selection.
- Rename 'Colors' card heading to 'Custom'
- Add 'Publish Theme' button that opens a dialog to publish kind 36767 events
- Query and display user's published themes in the grid after presets
- Clicking a user theme enters editor mode, showing 'Update Theme' alongside
'Publish Theme' to update the existing event in place
- Exit editor mode when selecting a builtin theme or preset
When changing a color via the inline color picker in ThemeSelector,
the handler was passing bare CoreThemeColors to applyCustomTheme(),
which replaced the entire customTheme config and dropped the font
and background fields. Now spreads the existing customTheme before
applying the new colors, matching the pattern used by FontPicker
and BackgroundPicker.
When visiting a profile that has theme colors but no font tag,
the effective font was undefined, which removed the font override
entirely — letting the visitor's custom font bleed through. Now
profiles without an f tag explicitly default to Inter.
When viewing your own profile and your custom theme differs from the
published one, the palette button now opens a dropdown with 'Update
Profile Theme' and 'Delete Profile Theme' options instead of directly
opening the update dialog. The initial share scenario (no published
theme yet) keeps the single-click tooltip behavior.
The profile page useEffect cleanup was restoring the user's theme from
stale closure values captured before Copy Theme was clicked. Use a ref
to always read the latest ownTheme/ownCustomTheme/configuredThemes at
cleanup time so the newly copied theme is correctly restored.
Instead of a standalone link that redirected to /settings/theme?import=,
the Copy Theme action is now a menu item inside the palette icon dropdown
on other users' profiles. It immediately applies the profile's theme as
the user's custom app theme (including font and background), and if
autoShareTheme is enabled, auto-publishes a kind 16767 event too.
Remove nostr from useEffect dependencies (replacing with a ref) so the
subscription is not torn down and re-opened on every render cycle.
Add a seenIds Set to guard against the same event ID firing multiple
notifications even if the relay delivers it more than once.
Delete the separate config/schema.ts that duplicated every Zod schema.
DittoConfigSchema is now simply AppConfigSchema.partial().strict(),
defined in src/lib/schemas.ts alongside everything else. vite.config.ts
imports it directly via a relative path.
Read an optional ditto.json (or CONFIG_FILE env) at build time, validate
it with Zod, and inject the result via Vite's define. At runtime the
build-time values are merged between the hardcoded defaults and user
localStorage, giving precedence: user > build-time > hardcoded.
Extract canonical Zod schemas into config/schema.ts (no path aliases)
so they can be imported from both vite.config.ts and runtime code.
Move AppConfigSchema out of AppProvider into src/lib/schemas.ts to
eliminate duplication.
Every place that previously read builtinThemes directly for light/dark
mode now calls resolveThemeConfig(), which checks config.themes first
and falls back to builtinThemes. This ensures configured themes are
actually used for colors, fonts, and backgrounds in all code paths:
AppProvider, useTheme, ThemeSelector, ThemeBuilderPage, ProfilePage.
Allow self-hosters to override the builtin light/dark themes by setting
a themes field in AppConfig with full ThemeConfig objects (colors, fonts,
backgrounds) for each mode.