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.
Add MiniAudioPlayer component with play/pause, seekable progress bar,
and time display. Profile fields whose value is a URL ending in an audio
extension (.mp3, .ogg, .wav, .flac, .aac, .m4a, .opus, .weba) now render
the player instead of a plain link, in both mobile and desktop views.
Replace ScrollArea with the exact useScrollCarets + ScrollCaret pattern
from SidebarMoreMenu — ResizeObserver tracks scroll state, ChevronUp/Down
carets appear above/below the list when there is more content, and
hovering them continuously scrolls at 8px per 16ms frame.
KindPicker component replaces the plain Select for the Kind filter:
- Mini search input at the top filters by label, description, or kind number
- Each row shows the Lucide icon from CONTENT_KIND_ICONS (same icons as
sidebar/more menu) at size-3.5 with the kind label
- Active item highlighted with primary color + checkmark
- Trigger button mirrors Select trigger style with icon + chevron
- Custom… option always available at the bottom
- Scrollable list capped at 216px so it doesn't overflow the popover
- 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
- Replace the raw text Input in the author filter with ProfileSearchDropdown
so users get the same typeahead + avatar dropdown as the main search bar
- Add hideCountry prop to ProfileSearchDropdown to suppress country suggestions
in contexts where they are irrelevant
- Author filter only writes to URL when a profile is explicitly selected from
the dropdown (immediate commit) — typing alone never updates the URL, so
slow per-keystroke relay queries no longer cause the filter to thrash
- Show a chip with the selected author's display name and an X to clear,
replacing the dropdown while an author is active
- Add commitAuthor / clearAuthor helpers that keep the URL param and the
display label in sync; resetFilters also clears the display label
- Persist all filters (replies, media, language, platform, kind, author) to URL params so they survive refresh and can be shared
- Add author scope filter: accepts npub or hex pubkey, filters both initial batch and live streaming
- Add reset all filters button in popover header + chip strip with clear link
- Show NIP-50 search string block below the input so users can see exactly what's sent to the relay
- Show active filter chips; empty state now lists active filters with a 'Clear all' shortcut
- Add kind + media type conflict warning when both are set simultaneously
- Fix Vines streaming: hardcoded 'return false' for kind 1 changed to only reject kind 1/1111, letting 22/34236 through
- Fix streaming language/protocol gap: protocol filter is now applied client-side to live streaming events via proxy tag detection
- Wire up useContentFilters: shouldFilterEvent() now applied in useStreamPosts useMemo so user-defined content rules affect search
- Add useDebounce hook (300ms) and apply to useSearchProfiles to stop hammering relays on every keystroke
Keep the $BTC format throughout — detection strips it internally for
matching, but the stored label and Select values all use the $ prefix
so the data round-trips correctly.
- Add leading spacer to Website and Lightning rows so they align with the
drag-handle column on custom fields
- Improve inferFieldType to detect Blossom-style URLs (SHA-256 hex paths)
as media, not just URLs with file extensions
- Replace 'Add Field' button with dropdown offering Text, Wallet Address, and Media options
- Wallet fields show a ticker dropdown (BTC, ETH, SOL, etc.) as the label
- Media fields include an upload button that fills the URL value via Blossom
- All custom fields are drag-and-drop reorderable using @dnd-kit
- Existing fields auto-detect their type when loaded from profile data
- 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
Muted threads now display as clickable links (shortened event ID with
an external link icon) that navigate to the thread via its nevent
identifier, instead of showing a raw unclickable hex string.
Closes#50