Two fixes:
1. useCustomEmojis now resolves ['a', '30030:...'] references in the
user's kind 10030 emoji list, fetching the referenced kind 30030
emoji pack events and merging their emoji tags. Previously only
inline emoji tags were extracted, missing subscribed packs.
2. EmojiPicker theme detection now reads the --background CSS variable
lightness instead of checking for .dark class on <html>. Custom
themes set class='custom' (not 'dark'), so the dark class check
always returned false. Now works for all theme modes: light, dark,
system, custom, and profile themes.
Add collapseAfter prop to AncestorThread. When set, ancestors beyond that
depth are hidden behind a 'Show earlier posts' button with a dotted thread
indicator. Reactions pass collapseAfter=1 so only the reacted-to post is
immediately visible.
The parentEventId was only computed for text notes (isTextNote), so kind 7
reactions never resolved their e-tag parent. Include isReaction in the
condition so the AncestorThread renders the reacted-to post above.
Add min-h-10 and flex items-center on the content side so the text line
vertically centers against the size-10 emoji bubble. Bump inline avatar
from size-5 to size-6 for better visual balance.
Replace the standalone emoji bubble with the user's normal avatar plus a
small reaction emoji badge at the bottom-right corner. This keeps the
avatar column aligned with other threaded posts while still making the
reaction visually distinct.
ThemeBuilderPage only tracked colors in local state, so importing,
editing, previewing, saving, and publishing a theme all dropped the
font and background properties. Now all code paths pass the full
ThemeConfig (colors + font + background) through applyCustomTheme
and into publishTheme/setActiveTheme.
Reaction events now display with a prominent emoji bubble, small inline
avatar, author name, and 'reacted' label — making them visually distinct
from regular posts in both thread ancestors and standalone detail views.
Kind 10030 is a replaceable event that may only exist on relays the
user published to. The default pool (user's NIP-65 relays) may not
have it. Now queries both the default pool AND a broader set of
well-known relays (purplepag.es, relay.nostr.band, etc.) in parallel,
picking the most recent event across both sources.
The ContentTypeRow only rendered a Switch for entries with feedKey.
Entries with feedOnly=true and showKey (like Custom Emojis) had no
toggle rendered. Added a fallback branch that renders a Switch using
showKey for these display-only toggles.
Integrate custom emojis from the user's kind 10030 emoji list into
the emoji picker, compose flow, reactions, and DMs:
Hook:
- useCustomEmojis: query kind 10030, extract emoji shortcode/url pairs
EmojiPicker:
- Accept customEmojis prop, render as dedicated 'Custom' tab via
emoji-mart's custom categories feature
- Changed onSelect from string to EmojiSelection union type
(native unicode | custom shortcode+url)
ComposeBox:
- Pass custom emojis to picker, insert :shortcode: on selection
- Scan content on publish for :shortcode: patterns, cross-reference
against user's emoji list, add ['emoji', name, url] tags (NIP-30)
QuickReactMenu:
- Custom emoji reactions: content=':shortcode:', with emoji tag
- Optimistic cache update includes custom emoji URL/name
DMChatArea:
- Pass custom emojis to picker, insert :shortcode: text
Settings:
- Registered as 'Custom Emojis' in Other Stuff > Whimsy section
- showCustomEmojis toggle (default true) gates all custom emoji
features — when disabled, no kind 10030 queries are made
- Fix pre-existing Bot icon missing import in sidebarItems
Activity feed now shows accepted, tentative, and declined RSVPs with
status-specific display:
- accepted: 'is going to' + green 'Going' badge
- tentative: 'might go to' + amber 'Maybe' badge
- declined: 'can't make it to' + muted 'Can't Go' badge
Previously only accepted RSVPs were shown, filtering out potentially
useful social signal from the follow list.
RSVPs whose calendar event fails to load from relays showed as
orphaned 'Alice is going to an event' headers with no event card
below. Now filtered out — only items with a successfully fetched
referenced event are displayed.
The relay pool returns non-deterministic results — sometimes the first
batch of 50 kind 31925 events contains only declined/tentative/malformed
RSVPs with zero accepted ones. The UI showed an empty state because the
scroll sentinel was never rendered, so the next page never fetched.
Now auto-fetches up to 5 additional pages when the accumulated results
have zero accepted RSVPs, ensuring we dig through the relay's data
before giving up.
The events entry in extraKinds only registered kind 31923 (time-based),
so date-based calendar events (kind 31922) were missing from the
general feed. Added extraFeedKinds field to ExtraKindDef and used it
to include 31922 alongside 31923 when feedIncludeEvents is enabled.
Registers 'ai-chat' in SIDEBAR_ITEMS with the Bot icon, linking to
/ai-chat. Requires authentication since the Shakespeare API needs a
logged-in Nostr account.
The intermittent empty results were caused by relay non-determinism —
different relay responses returned different subsets of RSVP events.
The code logic is correct; the variability is expected with the Nostr
relay pool.
The tool now accepts the complete theme configuration:
- Colors (required): background, text, primary as HSL strings
- Font (optional): any of the 17 bundled font families
- Background image (optional): URL with cover/tile mode
The system prompt, tool executor, and badge UI are all updated to
reflect the richer theme capabilities.
Two bugs causing the Activity tab to appear empty:
1. The 'no follows' early return fired while useFollowList was still
loading. followedPubkeys was undefined (loading), which made
hasFollows false, showing 'Follow some people...' even when the
user has follows. Now checks followsLoading before showing that
message — falls through to skeleton loading state instead.
2. showSkeleton used rsvpQuery.isPending which in TanStack Query v5
is true for disabled queries AND on query key changes (causing
skeleton flash during refetch). Changed to rsvpQuery.isLoading
which is only true during the actual first fetch, so stale data
shows while refetching.
The tool now accepts background, text, and primary as HSL color strings
and calls applyCustomTheme to set a fully custom theme, rather than
just switching between light/dark/system presets.
The follower-rsvps query was unreliable due to:
1. Stale closure: followedPubkeys was captured at render time but the
queryKey only included the user pubkey, not the actual follow list.
When the follow list loaded asynchronously, the queryFn could run
with an empty or stale array. Now the queryKey includes a stable
hash of the first 20 followed pubkeys so it re-runs when the
follow list actually changes.
2. followedPubkeys defaulting to []: when followData was undefined
(still loading), the default [] made hasFollows false, but on the
next render when followData arrived, the query might not re-fetch
because the stale cached result from the empty run was returned.
Now followedPubkeys is undefined until loaded, and isReady properly
gates the query.
The React warning about setState during render in useLayoutOptions is
a known pattern used by all pages and is not specific to EventsFeedPage.
Adds /ai-chat route with a Shakespeare AI chat interface that supports
OpenAI-compatible tool calling. Includes a set_theme tool that lets the
AI switch between light, dark, and system themes on the user's behalf.
Three issues causing intermittent empty Activity tab:
1. Query key collision: Activity used ['event-rsvps', ...] which
collided with useEventRSVPs hook. When the detail page invalidated
event-rsvps caches, it wiped the activity feed too. Renamed to
['follower-rsvps', ...].
2. Pagination breaking on filtered results: fetched 30 RSVPs but
filtered for accepted-only after. If a batch had mostly declined/
tentative, getNextPageParam saw 0 results and stopped paginating.
Now tracks rawCount separately and continues paginating as long as
raw events were returned.
3. Missing author+event deduplication: multiple RSVP updates from the
same user for the same event created duplicate activity items. Now
deduplicates by pubkey+eventCoord, keeping the latest.
Also fixed usePublishRSVP to use a stable d-tag (the event coordinate)
so updating an RSVP replaces the previous one rather than creating a
new addressable event each time.
Implement full calendar event rendering, dedicated events page, and
RSVP functionality for NIP-52 calendar events:
Registration:
- FeedSettings: showEvents + feedIncludeEvents toggles
- extraKinds: kind 31923 registered as 'events' in social section
with link to plektos.app for event creation/management
- Sidebar: Events entry with CalendarDays icon at /events
- Onboarding: added to content picker, pre-checked by default
Components:
- CalendarEventContent: rich card with cover image, title, date/time,
location, description, participant count, hashtags. Handles both
kind 31922 (date-based) and 31923 (time-based) with Intl formatting
- CalendarEventDetailPage: full detail view with RSVP UI (Going/Maybe/
Can't Go buttons + optional note), attendee sections with avatar
stacks, participant roles, share button, zap support
- RSVPAvatars: reusable overlapping avatar stack with tooltips
Hooks:
- useEventRSVPs: query kind 31925 RSVPs for an event, deduplicate by
author, group by status (accepted/declined/tentative)
- useMyRSVP: check current user's RSVP status for an event
- usePublishRSVP: publish kind 31925 RSVP with cache invalidation
Pages:
- EventsFeedPage: two-tab layout — Upcoming (calendar events from
follows/global with future-first sorting) and Activity (social feed
showing 'Alice is going to [Event]' from followed users' RSVPs)
Wiring:
- NoteCard: isCalendarEvent detection + CalendarEventContent dispatch
- PostDetailPage: routes kind 31922/31923 to CalendarEventDetailPage
- AppRouter: /events route
Kind 1222 (voice reply to kind 1) was missing from useReplies, and
kind 1244 (NIP-22 voice reply) was missing from useComments. Voice
messages were published correctly but never appeared because query
filters didn't include their kinds.
- useReplies: [1] -> [1, 1222]
- useComments: [1111] -> [1111, 1244]
- useWallComments: [1111] -> [1111, 1244]
- VinesFeedPage: [1111] -> [1111, 1244]
- useNotifications: added 1222, 1244
- useHasUnreadNotifications: added 1222, 1244
- useNativeNotifications: added 1222, 1244 with 'New voice message' title
The avatar was confusing as a dual-purpose element. Now uses a clean
circular play/pause button with primary color theming, separate from
the author info already shown by NoteCard's header.
Implement recording, publishing, and rendering of NIP-A0 voice messages:
- VoiceMessagePlayer: compact waveform bar player when imeta waveform
data is available, falls back to existing AudioVisualizer sinewave
- useVoiceRecorder: MediaRecorder hook with real-time amplitude capture,
60s max duration, and waveform sampling for NIP-A0 imeta tags
- ComposeBox: mic button in toolbar with recording UI (live waveform,
timer, cancel/send controls), publishes kind 1222 (root) or 1244
(NIP-22 reply) with imeta waveform and duration fields
- NoteCard + PostDetailPage: render kind 1222/1244 via VoiceMessagePlayer
- Feed settings: voice messages registered in Other Stuff > Media with
feed toggle (feedIncludeVoiceMessages), defaults to off
Cap the dialog at 85dvh with overflow-y-auto so it scrolls when
content is tall. Also add a hard max-h on the post preview text
to prevent line-clamp-3 failures with nested block elements from
NoteContent.