Letters were completely absent from the notification pipeline — users had
to visit the Letters page to discover incoming letters. This integrates
kind 8211 into every layer of the notification system:
- useNotifications: query, grouping, and referenced-event exclusion
- useHasUnreadNotifications: unread dot indicator
- NotificationsPage: LetterNotification component with link to /letters
- NotificationSettings: toggleable Letters row
- notificationTemplates: web push template
- Android NotificationRelayService + NostrPoller: native push support
- EncryptedSettings + schema: letters preference field
Closes#188
- Port letter protocol (kind 8211, NIP-44 encrypted) from lief
- LettersPage at /letters with inbox and sent tabs
- ComposeLetterSheet with full stationery, font, frame, sticker, drawing support
- LetterCard with expand-to-read animation and deletion
- LetterPreferencesSection stored in encrypted settings (NIP-78)
- /settings/letters route for letter preferences
- Letters added to sidebar nav
- All letter lib utilities: letterTypes, letterUtils, colorUtils extensions, sanitizeSvg, svgDrawing
- StationeryBackground, StationeryPicker, FramePicker, StickerPicker, DrawingCanvas all ported
- Fix incorrect npub in publish-achievement-badges.sh (was wrong bech32 encoding)
- Show actual badge names in award notifications by fetching kind 30009 definitions,
with fallback to un-slugified d-tag identifier
- Strip price/supply UI from shop page and badge detail (no purchase flow exists)
- Document custom badge tag extensions and DVM claim-achievement flow in NIP.md
- Remove unused badge-glow CSS animation (duplicate of pending-glow in Tailwind)
- Make badge award notifications respect user preferences instead of force-enabled
- Add badges toggle to notification settings page
- Group reactions, reposts, and zaps on the same post into a single condensed
row with stacked actor avatars (up to 5 + overflow count); single-actor
interactions use the original compact layout
- Deduplicate actors by pubkey within groups
- Sum zap totals across all actors in a zap group
- Add notificationPreferences per-type settings (reactions, reposts, zaps,
mentions, comments, onlyFollowing) to EncryptedSettings and Zod schema
- Per-type kind toggles and onlyFollowing applied at relay query level (kinds
array + authors filter), not client-side; query key includes kindsKey and
authorsKey so preference changes trigger a fresh fetch
- Default push notifications to enabled
- Notification settings UI matches ContentSettings exactly: section headers
with accent underline, p-4 wrapper, broken border rows (border-b on outer
wrapper div), sub-section labels, w-[52px] switch column, KindBadges
- Add 'Only from people I follow' toggle under Filter sub-section
- Filter and push notification prefs also respected in useNativeNotifications
web push subscription via stable ref
- Add NIP-51 Follow Sets management (useUserLists, UserListsPage at /lists)
- Add Follow Packs support (useFollowPacks) for curated people lists
- Add FeedEditModal for creating/editing home feed tabs with author scope,
kind picker, and list/pack author population
- Add AddToListDialog accessible from the note more-menu
- Unify home feed tab resolution with profile tabs via useResolveTabFilter,
supporting $follows variable expansion
- Wire People filter in Search and FeedEditModal with list/pack picker
- Extract shared ScopeToggle, ListPackPicker, parseSelectedKinds primitives
into SavedFeedFiltersEditor to eliminate duplication across three call sites
- Cache savedFeeds in AppConfig for instant tab render on load
- Strip orphaned profile-destination entries from encrypted settings on read
- SavedFeed type with destination field ('feed' | 'profile'), stored in
EncryptedSettings and synced via NIP-78 across devices
- useSavedFeeds hook: add/remove/rename, accepts destination param
- Search page 'Add to…' popover with two tile options:
- 'Home feed' — saves as a tab on the home page feed (always available)
- 'Profile tab' — only shown for generic searches (no author filter set);
auto-locks authorPubkey to the current user's pubkey so the tab shows
your own posts matching that search; user-specific searches are
ineligible since they already have an author scope
- Home feed (Feed.tsx) renders saved feeds as extra scrollable tabs after
Follows/Community/Global, powered by useStreamPosts (live streaming)
- Profile page (ProfilePage.tsx) renders saved feeds where destination=profile
and authorPubkey matches the viewed profile as custom tabs after Wall;
tabs are scrollable, use shrink-0 layout to avoid stretching
- Both tab bars are horizontally scrollable to accommodate many tabs
- ProfileSavedFeedContent and SavedFeedContent share the same stream-based
rendering: skeleton loading, empty state, live NoteCard list
- Add homePage config field (default: 'feed') that determines which
sidebar item renders at the '/' route
- Any sidebar item can be set as the homepage via Feed settings
- The feed is always accessible at /feed when not configured as home
- Normalize sidebar item IDs: every item follows path === '/' + id
(feed path changed from '/' to '/feed', emoji-packs renamed to emojis)
- Remove BOTTOM_NAV_ITEMS filter from MobileDrawer so all sidebar
items are visible on mobile (feed, notifications, search included)
- Mobile bottom nav dynamically reflects the configured homepage icon
- Homepage setting syncs across devices via encrypted settings
- Add emoji-packs -> emojis migration for existing sidebar orders
Sentry SDK is loaded via dynamic import() only when a DSN is configured
and error reporting is enabled, keeping it out of the initial bundle.
Users can control error reporting via a toggle and DSN field in
Advanced Settings, with the DSN synced across devices via encrypted
NIP-78 settings. The ErrorBoundary now reports fatal crashes to Sentry
with component stack context, and a beforeSend hook censors nsec
private keys before any data leaves the browser.
- Remove defaultZapComment config; derive from appName instead
- Add appId (string) for NIP-78 d-tag prefixes (e.g. ${appId}/metadata)
- Add client (optional NIP-89 addr) as third element of client tags
- Update useNostrPublish and useEncryptedSettings to use config.appName
and config.client for client tags instead of location.hostname
- Replace hardcoded 'ditto-metadata' d-tag with ${config.appId}/metadata
- Remove Default Zap Comment setting from AdvancedSettings UI
All runtime references to 'Ditto' now use config.appName, which defaults
to 'Ditto' and can be overridden at build-time via ditto.json. This
covers useSeoMeta titles across all pages, the welcome screen, feed CTA,
Nostr Connect app name, encrypted settings event title, and the
AdvancedSettings zap comment placeholder.
- Add notificationsEnabled to EncryptedSettings schema and interface
- NotificationSettings now persists the toggle to encrypted settings (synced across devices) instead of ephemeral local state
- useNativeNotifications: Capacitor path skips DittoNotification.configure when notificationsEnabled is false
- useNativeNotifications: web/PWA path opens a live Nostr req subscription and fires browser Notification API events when permission is granted and the setting is enabled
- Add ThemeConfig wrapper type (colors + fonts + background) replacing bare CoreThemeColors
- Sync theme event parsing/building with NIP.md spec: colors in c tags (hex), fonts in f tags, background in bg tags (imeta-style), empty content field
- Legacy backward compat: parse old JSON-in-content format and bare CoreThemeColors in localStorage/encrypted settings
- Bundle 10 curated fonts via fontsource (Inter, DM Sans, Outfit, Montserrat, Lora, Merriweather, Playfair Display, JetBrains Mono, Comfortaa, Comic Neue) with lazy dynamic imports
- Create font loader (src/lib/fontLoader.ts) for @font-face injection from remote URLs and CSS override application for title/body roles
- Rewrite FontPicker as dual title/body picker using bundled fonts instead of Google Fonts CDN
- Remove Google Fonts integration (useGoogleFont.ts, GoogleFontLoader.tsx)
- Resolve bundled font family names to CDN URLs when publishing to Nostr
- Apply profile theme fonts when visiting other users' profiles
- Update all consumers: AppProvider, useTheme, usePublishTheme, useEncryptedSettings, ThemeSelector, ThemeBuilderPage, ProfilePage, EditProfileForm, InitialSyncGate, NostrSync
Replace the 19-token ThemeTokens interface with a 4-field CoreThemeColors
for config storage, Nostr events, and encrypted settings. All other
Tailwind CSS tokens (card, muted, border, etc.) are now derived
automatically via deriveTokensFromCore().
- Add CoreThemeColors interface: { background, text, primary, secondary }
- Keep ThemeTokens as internal type for CSS variable injection
- Update builtinThemes and themePresets to store CoreThemeColors
- Add backward-compatible parsing for legacy 19-token format in Nostr
events, encrypted settings, and localStorage
- Simplify ThemeBuilderPage: remove Surface/UI color sections, just 4
color pickers with auto-derivation always on
- Update all consumers: ThemeSelector, ScopedTheme, ThemeUpdateCard,
MobileDrawer, LeftSidebar, ProfilePage, InitialSyncGate, etc.
- Advance lastSyncedTimestamp when skipping due to recentlyWritten() so the
same snapshot cannot be re-applied once the write window expires
- Use pendingSettings ref in useEncryptedSettings to accumulate rapid mutations
instead of reading stale cache data
- Fix ContentSettings handleToggle to not merge against stale nostrSettings
Restructure the theme architecture so 'light', 'dark', and 'system' are
builtin themes with build-time colors (customizable by self-hosters),
while 'black', 'pink', and future themes become presets that apply to a
single 'custom' theme slot.
Key changes:
- Theme type is now 'light' | 'dark' | 'system' | 'custom'
- builtinThemes (light/dark) and themePresets (black/pink/etc.) are
separate maps in themes.ts
- AppConfig gains optional customTheme (ThemeTokens) field
- useTheme exposes applyCustomTheme(tokens) for applying presets
- Custom theme tokens sync across devices via encrypted settings
- Backward compat: legacy 'black'/'pink' values in localStorage and
encrypted settings auto-migrate to custom + preset tokens
- All UI surfaces updated: ThemeSelector, LeftSidebar dropdown,
MobileDrawer cycle, and onboarding ThemeStep
Three bugs caused settings toggles to get stuck or reset:
1. useEncryptedSettings: onSuccess was updating the parsedSettings cache
under the old event ID, causing NostrSync to re-apply the stale remote
event when the new one arrived with a different ID. Fixed by using the
signed event's ID for both cache entries.
2. ContentTypeRow/SubKindRow: handleToggle was building the Nostr payload
from the local React feedSettings state, which lags behind on rapid
successive toggles. Fixed by merging against the Nostr-cached settings
(nostrSettings?.feedSettings) as the base instead.
3. NostrSync: lastSyncedTimestamp ref reset to 0 on every page reload, so
the 10-second recentlyWritten() guard was the only protection against
stale relay events overwriting local changes. Fixed by seeding the ref
with the current remote lastSync on first settings load, skipping
application of any event that isn't strictly newer than what was
already in the cache.
After updating encrypted settings, the onSuccess handler was calling
invalidateQueries which triggered a relay refetch. The relay would often
return the old event (before the new one propagated), causing NostrSync
to see a new lastSync timestamp and overwrite the locally-set theme
with the old value from Nostr.
The setQueryData call already keeps the cache up to date in-place,
so the invalidateQueries was purely counterproductive.
Encrypted settings synced via NIP-78 were parsed with a bare JSON.parse()
and cast with 'as EncryptedSettings', allowing invalid data (e.g. theme as
an object instead of a string) to pass through unchecked and crash the UI
when applied to classList.add(). Now both useEncryptedSettings and
useInitialSync validate decrypted JSON with a shared EncryptedSettingsSchema,
gracefully dropping invalid fields instead of propagating them.
Respect content-warning tags on events. Users can choose how to handle
them in Settings > Content > Sensitive Content:
- Blur (default): Shows a grey placeholder overlay with the warning
reason in quotes. Media is not loaded until the user clicks reveal.
- Hide: Removes content-warned posts from the feed entirely.
- Show: Ignores content warnings and displays everything normally.
The setting syncs across devices via encrypted NIP-78 settings.
This reverts the codebase back to the state at commit da5835b.
Reverted 1 commit(s):
- e905087: Change client tag to 'Mew' instead of hostname
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Use 'Mew' as the client name for encrypted metadata events instead of the deployment hostname, providing consistent client identification across all deployments.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Changed from 'mew-settings' to 'mew-metadata' to better reflect that this event stores various types of app state including settings, notification cursor, and other metadata.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Set staleTime and gcTime to Infinity for encrypted settings queries to prevent constant refetching and decryption requests. Queries only run once on mount and when explicitly invalidated after writes. This fixes the spam of kind 30078 signature requests.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Include SVG URLs in link preview detection (LinkPreview checks content-type to handle both images and OG pages)
- Add proper imeta tags (NIP-92) for all media URLs in posts
- Store uploaded file metadata and use full NIP-94 tags in imeta
- Reduce encrypted settings decryption spam by disabling refetchOnWindowFocus and refetchOnReconnect
- Replace Switch with modern segmented control for Edit/Preview toggle
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add notificationsCursor to EncryptedSettings interface
- Create useNotifications hook to track read/unread status
- Update NotificationsPage to mark as read after 1 second
- Highlight new notifications with accent background and left border
- Add dot indicator to Bell icon in sidebar and mobile nav when unread
- Auto-mark notifications as read when viewing the page
- Sync notification cursor across devices via NIP-78
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add write timestamp guard to prevent NostrSync from overwriting
settings we just changed locally
- 10 second cooldown window after any local settings write
- Eliminates back-and-forth between local state and stale encrypted data
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Replace invalidateQueries with setQueryData on mutation success
- Prevents refetch -> NostrSync -> updateConfig -> re-render loop
- Local settings change is now the only render, no secondary flash
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Use nostr.event() directly instead of useNostrPublish mutation
- Sign and publish happens without React Query mutation lifecycle
- Prevents signer prompt from causing component re-renders and glitches
- Settings still save properly but UI stays stable during theme changes
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- NostrSync now loads encrypted settings from Nostr on login
- Settings sync to local AppContext (theme, feedSettings, useAppRelays)
- Use 5-minute staleTime for fresh page loads to pick up changes
- Settings refetch automatically after 5 minutes on new page load
- No expensive real-time subscriptions - just periodic refresh
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Remove silly public/private toggle - all mutes are now encrypted by default
- Update useMuteList to store everything in encrypted content field
- Remove isPrivate field from MuteListItem interface
- Simplify MuteSettings UI by removing encryption toggle
- Update muteHelpers to remove hasPrivate from summary
- Add useEncryptedSettings hook for unified app settings storage
- Integrate MuteSettings component into SettingsPage /settings/mutes tab
- Refactor useContentFilters to use unified encrypted settings
- All sensitive user preferences now encrypted with NIP-44
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>