Rename toggle to 'Sync app theme with your profile theme' and update
description to explain what disabling it actually does, so users
understand it unsyncs the two themes rather than just stopping posts.
Consistent wording across both ThemeSelector and ThemesPage.
Closes#80
- Add kind 1111 to the Nostr REQ filter in NotificationRelayService
- Add case 1111 to kindToAction() in NostrPoller with the same context-aware
label logic: 'replied to your comment' when k=1111, 'commented on your post' otherwise
When a kind 1111 comment's parent is another comment (k tag = '1111'), show
'Replying to @user' with a link to the parent comment instead of the generic
'Commenting on [root]' label that always pointed to the thread root.
- Include kind 1111 in all three notification query filters (useNotifications, useHasUnreadNotifications, useNativeNotifications)
- Add CommentNotification component with context-aware action label: 'replied to your comment' when parent kind is 1111, otherwise 'commented on your post'
- Add 'New comment' title for native/browser push notifications
- Include kind 1111 in the Mentions tab filter alongside kind 1
- Raised dialog z-index from z-[150] to z-[250] so the overlay sits above
CommentsSheet's backdrop (z-[200]) and receives touch events
- Removed stopPropagation from DialogOverlay/DialogContent onClick handlers
which was preventing Radix DismissableLayer's deferred click listener from
reaching document on iOS Safari
On iOS touch, Radix registers a one-time click listener on document after
pointerdown. The stopPropagation was intercepting that click before it could
reach document, so the dismiss callback never fired. Z-index stacking now
handles isolation between the dialog and the CommentsSheet backdrop.
The picker was positioned relative to the trigger button via a Popover,
which felt awkward especially on smaller screens or inside other modals.
Now renders as a centered Dialog with a backdrop for a more traditional
modal experience.
Radix Dialog's RemoveScroll blocks wheel events on elements outside the
Dialog's DOM tree. The GIF picker Popover was portaling to document.body,
placing it outside the RemoveScroll boundary. Trackpad scrolling was
blocked while scrollbar dragging still worked.
Added PortalContainerContext so PopoverContent can portal into the modal
element instead of document.body when rendered inside a dialog/modal.
- /lists now redirects to /settings/lists
- UserListsPage redesigned to match the settings sub-page pattern (back arrow to /settings, collapsible section, IntroImage)
- Lists added as an entry in the SettingsPage menu
- Removed Lists from sidebar nav (it was requiresAuth and felt out of place)
- 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
Blossom servers return content-addressed URLs (/<sha256>) that may omit
the file extension. After upload, extract the extension from the source
filename and append it to the URL if not already present. This helps
clients infer the media type from the URL alone.
Core tabs (Posts, Media, Likes, Wall, etc.) were published with empty
filter stubs, making the event uninterpretable by other clients. Now
each core tab carries a proper NIP-01 filter with the actual pubkey
interpolated (e.g. Wall: {kinds:[1111], #A:["0:<pubkey>:"]}).
The proprietary SavedFeedFilters JSON schema in kind 16769 tab events was
non-interoperable. Replace it with standard NIP-01 filter objects that any
Nostr client can execute, plus a variable system (var tags) for dynamic
values like follow lists.
- Tab filters are now standard NIP-01 filters with optional NIP-50 search
- Variables ($name) in filters are resolved via var tags that extract tag
values from referenced events (e.g. follow list pubkeys from kind 3)
- $me is the only runtime variable (profile owner's pubkey)
- Variables in arrays are expanded in-place (spliced)
- Add useResolveTabFilter hook for variable resolution with caching
- Update all consumers: ProfilePage, Feed, SearchPage, ContentSettings
- Remove SavedFeedFilters type entirely in favor of TabFilter
Simplify the ProfileTabEditModal to only show Search Query, Author Scope
(Me/Contacts/Global), and a multi-select Kind picker with checkboxes and
chips. Remove Media, Platform, and Language fields that were not useful
for profile tabs.
Extract parseKindFilter utility to handle comma-separated kind values
across all consumers (Feed, ProfilePage, SearchPage).
- enterTabEditMode now starts from exactly viewTabs (no auto-restore of
deleted default tabs)
- + and Check icons in the tab edit dropdown and save button use strokeWidth=4
- × remove button uses text-xl × character (no SVG blurriness)
- Fix Posts & replies active indicator by using CORE_TAB_IDS lookup for
both onSelect and active prop
- Visitor ... menu shows missing default tabs when profile has kind 16769
event with some defaults removed
- viewTabs derives from kind 16769 event: null=show all 5 defaults, []=show
nothing, [...]= show exactly those in that order
- handleSaveTabEdit publishes all tabs (core + custom) as the full ordered
list — core tabs stored as stubs with default filters
- enterTabEditMode starts from viewTabs + any missing core tabs at the end
- Deleting all tabs now correctly shows an empty tab bar (not the defaults)
- Encrypted settings fallback only triggers when data===null (no event yet)
- useProfileData no longer seeds profile-tabs cache (removes stale-seed race)
- usePublishProfileTabs invalidates both profile-tabs and profile-supplementary
- useProfileTabs returns null (no event) vs [] (event with no tabs) so
deletions are respected across page reloads
- Visitor '...' dropdown shows missing default tabs when a profile has a
published kind 16769 event that omits some defaults
Kind 16769 (profile tabs):
- src/lib/profileTabsEvent.ts: encode/decode kind 16769 — tab tags are
['tab', label, filtersJSON], no UUIDs, label is the stable key
- useProfileTabs: queries kind 16769 with authors filter (secure)
- usePublishProfileTabs: publishes kind 16769, invalidates cache
- useProfileSupplementary: now fetches kind 16769 alongside kind 3 and
10001 in a single relay round-trip, seeds profile-tabs query cache
- NIP.md: documents kind 16769 schema
ProfileTabEditModal:
- Save button is now full-width with a Check icon (Cancel below)
- hideFrom + hideSort props on SavedFeedFiltersEditor used here — author
is the profile owner implicitly, sort is irrelevant for profile tabs
ProfileTabsManagerModal:
- dnd-kit drag-to-reorder sortable list of custom tabs
- Per-row edit (pencil) and remove (trash) buttons
- Add custom tab button at the bottom
- Single Save button publishes the full updated list
ProfilePage tab bar (own profile):
- Custom tabs read from kind 16769 via useProfileTabs
- + button opens a DropdownMenu: core tabs listed as already-added
(dimmed with checkmarks), Add custom tab at the bottom
- SlidersHorizontal pencil at end of row opens the manager modal
(only shown when custom tabs exist)
- Per-tab onEdit pencil removed — manager handles all editing
- ProfileTab type aliased to CoreProfileTab to avoid collision with
CustomProfileTab from profileTabsEvent
- New src/components/SavedFeedFiltersEditor.tsx exports:
SavedFeedFiltersEditor (controlled), KindPicker, AuthorChip,
AuthorFilterDropdown, buildKindOptions, useScrollCarets
- SavedFeedFiltersEditor renders all filter controls: query input, author
scope toggle (Anyone/Follows/People with chips), sort toggle
(Recent/Hot/Trending), media/platform/language/kind selects
- ContentSettings SavedFeedRow now uses SavedFeedFiltersEditor in its
edit panel — clicking pencil expands the full filter set, not just a
query text box; Save writes label + all filters atomically
- SearchPage filter popover delegates to SavedFeedFiltersEditor
(showQuery=false); all duplicate local implementations removed
- useSavedFeeds gets updateSavedFeed(id, {label?, filters?}) for atomic
label+filters updates
- Add updateSavedFeed to useSavedFeeds (updates label and/or filters atomically)
- SavedFeedRow now has a collapsible edit panel below the summary row:
two inputs side-by-side (Tab name / Search query), Cancel + Save buttons
- Pencil icon toggles the panel open/closed; X in the panel header closes it
- Enter in the query field saves, Escape cancels
- The summary row (label + filter chips) always stays visible above the panel
- Remove button stays in the header row, hidden until hover
SavedFeedsSection appears inside the Feed Tabs collapsible in ContentSettings
after the Community block (only when feeds exist). Shows two groups:
'Home feed tabs' and 'Profile tabs', each listing saved feeds with:
- Feed label + compact filter summary (query, author scope, sort icon)
- Inline rename: click pencil, edit input, Enter/check to save, Escape to cancel
- Remove button (destructive hover, disabled while saving)
- Actions fade in on row hover to keep the list clean
- Icons: Home/User for group headers, Globe/Flame/TrendingUp/Clock for filter hints
Also removes pre-existing unused arrayMove import in ProfileSettings.tsx.