- Make LetterContent.body optional; a letter requires a non-empty body
or at least one sticker
- Replace colors:[]/flatMode with event-stripping: flat color moment =
event field stripped from Stationery, removing the colors? field
- Remove edgeScale: stickers render at their stored scale value with no
edge-proximity size reduction, matching lief and the NIP
- Fix sticker shrinking near card edges: add max-width:none to override
Tailwind preflight max-width:100% on img/svg elements
- Add hideKindHeader prop to NoteCard, used by ReferencedNoteCard to
suppress redundant action headers in repost/reaction/zap notifications
- Redesign badge award notification: show full BadgeContent showcase card
with prominent rounded-pill Accept Badge button below
- Redesign letter notification: larger centered envelope (minimal mode
hides name/timestamp), click opens LetterDetailSheet inline, View All
Letters button below
Stickers now scale down proportionally as their center approaches any
edge of the letter card, preventing them from overflowing the rounded
corners. A 5% drag buffer keeps sticker centers away from the very
edge. The sticker overlay uses pointer-events-none so the textarea
remains clickable underneath.
- Allow sending letters with only stickers/drawings (no body text required)
- Fix sticker drag positioning by using cardRef instead of page container
ref, so percentage coordinates map correctly to the card boundaries
- Preserve stationery source events (color moments/themes) in preferences
so they embed as gift attachments in sent letters
- Accept sticker-only letters during decryption validation
When followsFeedShowReplies is false, the relay limit was PAGE_SIZE (15)
but client-side reply filtering could discard most events, leaving only
a few visible posts per page with large time gaps between them.
Apply the same over-fetch pattern already used by useProfileFeed:
- Request PAGE_SIZE * 3 events when reply filtering is active
- Use rawCount (pre-filter) for pagination termination so pages where
all items are replies don't prematurely stop pagination
The Avatar component was initializing maskUrl as '' and loading it in a
useEffect. Since hasCustomShape was true immediately, rounded-full was
removed on the first render, but the mask wasn't applied until after the
effect fired — causing a visible square flash for one frame.
getAvatarMaskUrl is already synchronous (renders emoji to canvas, caches
the data-URL), so compute it inline during render instead of deferring
to an effect. The mask is now applied on the very first paint.
useLayoutOptions was calling store.setOptions() synchronously during
render, which triggered useSyncExternalStore listeners in MobileBottomNav
(and MainLayout) while Index was still rendering.
Move the store update into useLayoutEffect, which fires synchronously
after commit but before browser paint — same visual result without
violating React's setState-during-render rule.
Previously comments shared feedKey 'feedIncludePosts' with kind 1, and
generic reposts shared 'feedIncludeReposts' with kind 6. This made it
impossible to toggle them independently in settings.
Add feedIncludeComments and feedIncludeGenericReposts to FeedSettings
and wire them to their respective EXTRA_KINDS entries.
Multiple ExtraKindDef entries share the same feedKey (e.g. posts/comments
both use feedIncludePosts) and multiple subKinds share the same showKey
(e.g. both video sub-kinds use showVideos). Using these as React keys
caused 'duplicate key' warnings.
Use def.id (always unique) for ContentTypeRow keys and sub.feedKey
(unique per sub-kind) for SubKindRow keys.
Allow users to browse and restore previous versions of their accepted
badges (profile badges) from relay history, matching the existing
mute list recovery pattern in /settings/content.
The lazy-loading skeleton was missing center column borders
(sidebar:border-l/r) and the right sidebar widget backgrounds
(bg-background/85 rounded-xl). Updated to mirror the real Outlet
wrapper classes and RightSidebar widget card styling with three
distinct skeleton sections (Trends, Hot Posts, New Accounts).
When a badge list overflows PREVIEW_LIMIT, show one fewer badge to
make room for the +N button on the same row instead of widowing it.
The loading skeleton now also includes a placeholder for the overflow
cell when applicable.
Shows placeholder skeletons matching the badge grid layout (48px
rounded squares + name bars) instead of a centered spinner while
badge definitions are being fetched.
Kind 10008/30008 profile badges events now render a compact card with
author info, a row of up to 6 badge thumbnails, and a badge count
when embedded in quotes or reply context. Works in both EmbeddedNote
(nevent references) and EmbeddedNaddr (naddr references).
The KIND_HEADER_MAP action header (e.g. 'updated their badges',
'created a badge', 'shared a photo') was only rendered in the normal
NoteCard layout. Now it also appears in the threaded layout, so parent
events shown as ancestors in reply threads display their kind context.
When viewing a NIP-22 comment (kind 1111) that references its parent
via an 'a' tag (addr coordinates) rather than an 'e' tag (event ID),
the parent event is now rendered as a full threaded NoteCard with a
connector line — matching how kind 1 reply threads display ancestors.
Previously these showed a compact AddressableEventPreview banner.
Now the parent badges list (or any other addr-referenced event) renders
inline in the thread, giving proper visual context for the comment.
Replaceable events have no d-tag, so useAddrEvent must omit the #d
filter for kinds in the 10000-19999 range. Without this, querying
for a kind 10008 profile badges event via naddr would include
'#d': [''] in the filter, which fails to match events without a d-tag.
Replaceable events should use naddr encoding (kind + pubkey + empty
identifier) rather than nevent (event ID), since they are identified by
their coordinates. This fixes kind 10008 profile badge events linking
to /nevent1... instead of /naddr1... from the feed.
PostDetailPage (used for nevent1 identifiers) was missing the kind
10008/30008 branch, so profile badge events fell through to the generic
PostDetailContent. AddrPostDetailPage already had this handling via
ProfileBadgesDetailView — now PostDetailPage shares the same code path
for both badge definitions (30009) and profile badges (10008/30008).
Build the tilt directly into BadgeThumbnail instead of a separate
wrapper. Use aggressive parameters (35deg max tilt, 1.15x scale,
perspective = size*3) so the effect is clearly visible on small
28-48px thumbnails. Add a perspective parameter to useCardTilt.
Remove old group-hover:scale-110 from all badge grid call sites
(BadgeShowcaseGrid, ProfileBadgesContent, ProfilePage,
ProfileHoverCard) since the tilt+scale is now built into the
thumbnail itself.
Reuse useCardTilt for the badge image in BadgeContent feed cards, but
only respond to mouse/pen pointer events. Touch events are explicitly
ignored and touch-action is set back to auto so tapping through to the
badge detail view and normal scrolling are unaffected. Includes the
specular glare overlay masked to the badge image shape.
Update useCardTilt to handle touch inputs via PointerEvent. Touch
interactions use a press-and-drag gesture: the tilt follows the finger
while down, then holds for 600ms after release before smoothly
resetting. touch-action: none prevents the browser from intercepting
the gesture for scrolling. Mouse behavior is unchanged.
Update BadgeHero glare overlay to match: glare follows touch position
during the drag and fades after the same linger delay on release.
Remove the secondary Badge UI element showing '<Award icon> Badge' next to
the issuer name on the badge detail page. The context is already clear from
the page layout and hero image.
Remove the 'Name's Badges (N badges)' header line from ProfileBadgesContent
to reduce visual clutter. The badge grid and NoteCard's KIND_HEADER_MAP
already provide sufficient context.
Rename the checklist item from 'Inline embeds / quote posts' to
'Embedded note cards' with explicit file paths, explain that kinds
with tag-based media may need attachment indicator updates, and add
a note distinguishing EmbeddedNote components from the NoteCard
compact prop to prevent confusion.
- Add isPhoto detection and PhotoDetailContent in PostDetailPage so kind 20
events render their image gallery when viewed directly via nevent links
- Add parsePhotoUrls helper and ImageGallery import to PostDetailPage
- Add 'Photo' shell title for kind 20 loading state
- Add KIND_HEADER_MAP entry for kind 20 ('shared a photo') in NoteCard
- Add Photo attachment indicator in EmbeddedNote for kind 20 events in
quote posts and reply context
Profile badges should be a replaceable event (kind 10008), not an
addressable event (kind 30008) with a fixed d-tag. This follows the
same deprecation pattern used by NIP-51 lists.
All writes now publish kind 10008. All reads query both 10008 and
legacy 30008, picking whichever is newest, for backwards compatibility
during the transition period.
Extract shared fetchFreshEvent() utility that fetches the freshest
version of a replaceable/addressable event directly from relays before
every mutation. This prevents data loss when the TanStack Query cache
is stale (cross-device edits, rapid sequential operations).
Previously only useFollowActions and useMuteList had this safety
pattern. Now all list-type hooks use the same shared primitive:
useAcceptBadge, useRemoveBadge, useBookmarks, usePinnedNotes,
useInterests, and useUserLists.
Interests, custom emojis, and Blossom server list queries now batch
with profile/follow/mute queries instead of firing separate REQs,
reducing ~3 REQs on feed load.
The stream buffer count was reported raw without applying client-side
filters (search query, media type, replies, protocol, mute list, etc.),
so the pill would show e.g. '10 new posts' when only 3 matched the
active search criteria. Extract the filtering predicate into a shared
matchesFilters callback and derive the pill count from filtered buffer
contents instead of the raw streamBufferCount.
Both pages have a SubHeaderBar but were missing the ARC_OVERHANG_PX
spacer div that prevents content from sitting behind the arc background.
Every other page with tabs already includes this spacer.
Missing pull-to-refresh on Photos, Videos, Trends, Search, Bookmarks,
TagFeed (#t/#g), DomainFeed pages meant Android users had no way to
refresh content without navigating away.
- Create usePageRefresh hook that wraps queryClient.invalidateQueries
with a referentially-stable callback (ref-based) for PullToRefresh
- Wrap scrollable content in PullToRefresh on all affected pages
- Fix Feed.tsx: HashtagFeedContent, GeotagFeedContent, and
SavedFeedContent tabs now include PullToRefresh (were outside wrapper)
- Refactor Events, Books, Themes pages to use usePageRefresh for
consistency and reduced boilerplate
Clicks inside the portaled AlertDialog bubble through React's synthetic
event tree to the NoteCard article, triggering post detail navigation.
Adding stopPropagation on AlertDialogContent prevents any click inside
the delete confirmation from reaching the card handler.
MenuItem button clicks inside the Radix Dialog portal bubble through
React's synthetic event system to the parent article's handleCardClick,
causing navigation to post details when selecting menu actions like
delete. Adding stopPropagation prevents this.
Move the delete confirmation AlertDialog out of NoteMoreMenuContent
(where it was nested inside the more-menu Dialog) and into the parent
NoteMoreMenu component. The nested Radix dialogs caused overlapping
overlays and focus traps that left the page uninteractable after
confirming deletion. Now follows the same close-then-open pattern used
by Report, Mention, AddToList, and EventJson dialogs.