Commit Graph

38 Commits

Author SHA1 Message Date
Mary Kate Fain f82adab05d Fix: add letter (kind 8211) notifications
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
2026-03-27 14:15:50 -05:00
Chad Curtis 865fabce98 Add Letters inbox page with full letter composing from lief
- 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
2026-03-26 16:11:29 -05:00
Alex Gleason 064913d703 fix: address badge MR review feedback
- 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
2026-03-22 22:15:34 -05:00
Chad Curtis b76bf7b198 Condense notifications and add per-type notification preferences
- 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
2026-03-14 19:06:31 -05:00
Chad Curtis c7f3211b7d Add user lists, follow packs, home feed tabs, and list-based feed filtering
- 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
2026-03-06 17:23:47 -06:00
Chad Curtis 94878da5bc Add saved search feeds: home feed tabs and profile tabs
- 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
2026-03-05 21:14:30 -06:00
Alex Gleason 35bc1a0438 Add configurable homepage route and show all items in mobile sidebar
- 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
2026-03-05 15:23:43 -06:00
Alex Gleason f1191299e8 Add Sentry integration with lazy loading and user-configurable DSN
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.
2026-03-03 17:25:14 -06:00
Alex Gleason d0b49e78b6 Add appId/client config, use appName for client tags and zap comments
- 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
2026-03-01 13:44:31 -06:00
Alex Gleason 713bf52cfb Add configurable appName to AppConfig, replacing hardcoded 'Ditto' in page titles and UI text
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.
2026-02-28 17:09:37 -06:00
Chad Curtis f8acb1b630 Sync sidebar order, feed tabs, and advanced URLs to encrypted settings; remove sidebar column from content settings 2026-02-28 00:12:37 -06:00
Alex Gleason 14f75ed50d Add auto-share theme toggle: automatically publish custom theme changes to profile 2026-02-26 19:53:46 -06:00
Alex Gleason 92c1d98fe2 Merge branch 'main' of gitlab.com:soapbox-pub/ditto-mew 2026-02-26 16:22:53 -06:00
Chad Curtis fa250e6a19 Wire notifications toggle to encrypted settings; add PWA browser notification support
- 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
2026-02-26 15:05:47 -06:00
Alex Gleason 3b15044919 Implement ThemeConfig type system with bundled fonts and NIP.md-compliant event parsing
- 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
2026-02-26 14:46:30 -06:00
Alex Gleason 8d03d32b0d Simplify theme system to 4 core colors (background, text, primary, secondary)
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.
2026-02-25 22:46:27 -06:00
Alex Gleason 67f0dc32e7 Merge branch 'main' of gitlab.com:soapbox-pub/ditto-mew
# Conflicts:
#	src/components/AppProvider.tsx
#	src/hooks/useTheme.ts
2026-02-24 22:09:09 -06:00
Chad Curtis 2e6beb3c23 Fix settings toggles being overwritten by stale sync
- 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
2026-02-24 22:01:33 -06:00
Alex Gleason 7a85ec73ed Refactor theme system: separate builtin themes from custom presets
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
2026-02-24 21:46:28 -06:00
Chad Curtis e8fe66f91c Fix settings toggles resetting due to sync race conditions
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.
2026-02-24 20:24:43 -06:00
Alex Gleason f260b6192f fix: remove invalidateQueries after settings write to prevent theme revert
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.
2026-02-23 18:04:27 -06:00
Alex Gleason 1f035f4a6e Rename Mew to Ditto across entire codebase
- Update all branding: titles, meta tags, OG tags, manifest
- Rename MewLogo component to DittoLogo
- Update Capacitor config: appId to pub.ditto.app, hostname to ditto.pub
- Migrate Android Java package from com.mew.app to pub.ditto.app
- Rename MewNotificationPlugin to DittoNotificationPlugin
- Update all localStorage keys from mew: to ditto: prefix
- Update Nostr encrypted settings d-tag from mew-metadata to ditto-metadata
- Update all page titles, zap comments, and UI strings
- Rename ic_stat_mew.xml drawable to ic_stat_ditto.xml
- Update build script references (mew.apk -> ditto.apk)
- Update all domain references (mew.shakespeare.wtf -> ditto.pub)
2026-02-23 11:50:00 -06:00
Alex Gleason 0d48221476 Validate encrypted settings with Zod to prevent corrupted data from crashing the app
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.
2026-02-23 11:37:17 -06:00
Chad Curtis eb483469bb Add NIP-36 content warning support with blur/hide/show settings
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.
2026-02-20 16:45:33 -06:00
Alex Gleason 6180bbf0c3 Fix 30 ESLint errors: remove unused imports/variables, replace any types, use const 2026-02-19 20:14:01 -06:00
Chad Curtis 16e20df0cc Optimize feed loading performance from 8s to 1.3s
- Delay non-critical queries (notifications, sidebar, sync) to prioritize feed
- Wait for batch author/stats queries before rendering NoteCards
- Disable page 2 auto-fetch to reduce initial query contention
- Fix notification badge flicker and sidebar 'No trends' flash
- Sequential loading of authors then stats to avoid relay saturation

84% performance improvement. Requires NPool patch: comment out
Promise.allSettled in node_modules/@nostrify/nostrify/dist/NPool.js:101
2026-02-19 04:13:02 -06:00
shakespeare.diy d2dc4550db Revert to da5835b: Rename encrypted settings d-tag to mew-metadata
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>
2026-02-18 23:37:14 -06:00
shakespeare.diy e90508768f Change client tag to 'Mew' instead of hostname
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>
2026-02-18 23:36:46 -06:00
shakespeare.diy da5835b77d Rename encrypted settings d-tag to mew-metadata
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>
2026-02-18 23:36:05 -06:00
shakespeare.diy 9c3e0783b1 Prevent encrypted settings spam with aggressive caching
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>
2026-02-18 23:28:51 -06:00
shakespeare.diy a45838f7d2 Fix preview toggle and reduce notification signature spam
- 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>
2026-02-18 23:20:00 -06:00
shakespeare.diy e7e4a1fa5c Add encrypted notification tracking with visual indicators
- 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>
2026-02-18 20:54:01 -06:00
shakespeare.diy 883bf893cf Fix settings spasm by skipping NostrSync after local writes
- 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>
2026-02-18 03:44:06 -06:00
shakespeare.diy abf6be233f Fix settings glitch by updating cache in-place instead of refetching
- 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>
2026-02-18 03:42:32 -06:00
shakespeare.diy dd5021788f Publish encrypted settings in background without triggering re-renders
- 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>
2026-02-18 03:40:01 -06:00
shakespeare.diy f46e7facf1 Change encrypted settings stale time to 30 minutes
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:30:15 -06:00
shakespeare.diy 93e6848622 Add encrypted settings sync on login with stale-while-revalidate
- 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>
2026-02-18 03:29:21 -06:00
shakespeare.diy 9400b0392a Refactor mute settings to always use encryption
- 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>
2026-02-18 03:22:05 -06:00