Replace /#\w+/g with /#[\p{L}\p{N}_]+/gu across all hashtag regexes
so that hashtags like #Bíblia and #verdade parse correctly. Affects
NoteContent, BioContent, ComposeBox, and PhotoComposeModal.
Extract shared useInsertText hook to DRY up the duplicated text
insertion logic across ComposeBox, DMChatArea, and ZapDialog.
Add EmojiPicker (GUI) and EmojiShortcodeAutocomplete (:shortcode
typing) to the zap comment textarea, and also add shortcode
autocomplete to the DM chat input which was previously missing it.
Closes#176
Remove the inline toggle from the post composer toolbar and add
imageQuality to AppConfig instead. The setting now lives in
Settings > Network under 'Image Uploads' with a Compressed/Original
pill toggle, persisted to localStorage like other app settings.
Adds a Compressed/Original toggle button in the ComposeBox toolbar.
When set to Original, images are uploaded as-is without resizing or
re-encoding. Defaults to Compressed.
Images attached via ComposeBox are now resized (max 1920px) and
converted to JPEG before uploading, matching the BackgroundPicker
behavior. Non-image files (audio, video, .xdc) are unaffected.
Blurhash is computed from the resized file for consistency.
Add the attribute to the file input in ComposeBox and update
the onChange handler to iterate over all selected files, uploading each
one via handleFileUpload, enabling users to select and attach multiple
files at once.
Addressable events (kinds 30000-39999) like lists get new event IDs when
updated, causing nevent references to break. Use naddr coordinates
(kind:pubkey:d-tag) which remain stable across updates.
- avatar.tsx: consolidate to single isEmojiShape boolean, fix AvatarFallback using same logic as Avatar
- avatarShape.ts: accept NostrMetadata directly in getAvatarShape (no cast needed)
- Remove 'as Record<string, unknown>' casts from all ~50 call sites
- Replace 'circle' magic string with empty string in form defaults and parseShape
- Use isValidAvatarShape instead of string comparison in save logic
- ProfileCard: extract IIFE overlay style into useMemo, use isEmojiShape throughout
- New 'shape' property on kind 0 profile metadata with 7 predefined shapes:
circle, triangle, inverted-triangle, hexagon, star, inverted-star, hexagram
- Avatar component updated with clip-path support for non-circle shapes
- Visual shape picker added to both EditProfileForm and ProfileSettings
- Shape applied across all 49 avatar render sites in the app
- New UserAvatar wrapper component for future simplified avatar rendering
- Unknown shape values gracefully fall back to circle (forward compatible)
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.
All shortcode regexes used [a-zA-Z0-9_] which excluded hyphens,
causing custom emojis like :GM-Chachi: to fail at every stage:
publishing (no emoji tag added to event), rendering (shortcode not
resolved to image), and detection (not recognized as custom emoji).
Updated the character class to [a-zA-Z0-9_-] across all 8 locations
in NoteContent, CustomEmoji, ComposeBox, and customEmoji utility.
Also added viewer's custom emoji collection as a fallback in
NoteContent so shortcodes render even when events from other clients
omit the emoji tag.
Closes#62
The composer preview tab was missing webxdc embeds because it manually
rendered only videos and audio, while webxdc rendering lived exclusively
inside NoteCard. Extract NoteMedia into a shared component and
ImetaEntry/parseImetaMap into a shared utility, then wire them into the
ComposeBox preview with proper imeta tag generation on the mock event.
- ComposeBox: invalidate the correct 'event-comments' cache key after
posting a NIP-22 comment so CommentsSheet refreshes immediately
- toast: raise z-index to 300 so it renders above the CommentsSheet
backdrop blur (which sits at z-200)
- Remove CustomEmojiPicker.tsx and useUserEmojiPacks.ts (replaced by
Derek's useCustomEmojis + emoji-mart custom categories)
- Update EmojiPackContent to use local kind 10030 query
- Update EmojiShortcodeAutocomplete to use useCustomEmojis
- Fix quick reaction buttons to render custom emojis as images
- Fix quick reaction buttons to include emoji tag when reacting
- Filter stale custom emojis from quick reaction row
- Add emoji tags to compose box mock event for preview rendering
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
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
- Add AudioVisualizer component: mirrors VideoPlayer chrome exactly (same overlay controls, progress bar, hide-on-idle, scroll-pause) with a canvas sinewave and author avatar in the centre
- Wire audio into NoteCard (kind 1 notes), FileMetadataContent (kind 1063), and ComposeBox preview
- Extract shared utilities: formatTime, usePlayerControls hook, and mediaUrls (regexes + mimeFromExt) to eliminate duplication across VideoPlayer, AudioVisualizer, NoteCard, NoteContent, ComposeBox, and EmbeddedNote
- Fix ComposeBox: audio URLs now get correct imeta MIME tags and are excluded from link-embed detection
Introduces a GIF picker powered by Tenor's API with search and trending
GIFs. The picker is integrated into both the post compose toolbar and
the DM chat input area, alongside a new emoji picker for DMs.
Widen ComposeBox.replyTo and ReplyComposeModal.event to accept URL,
enabling the standard post composer for NIP-22 comments on external
content. Replace the inline CommentForm on /i/ pages with a FAB that
opens ReplyComposeModal, giving users the full compose experience
with file uploads, emoji picker, and content warnings.
Use the same NoteCard-based threaded comment display on /i/ pages
that the rest of the app uses for post detail views. This replaces
the custom recursive Comment tree with the standard threaded layout
(direct replies + first sub-reply preview).
Also fix PostDetailPage and ComposeBox to handle I (external content)
tags when reconstructing the root for kind 1111 comments, enabling
proper threading when viewing or replying to comments on URLs.
Use useComments hook to fetch NIP-22 comments for non-kind-1 events
instead of useReplies (which only fetches kind 1), and render them
with the same NoteCard threaded layout used for kind 1 replies.
Also invalidate comments cache when posting NIP-22 replies.
Removes duplicated NIP-22 tag logic from ComposeBox and delegates to
usePostComment, which correctly handles addressable/replaceable events
without adding spurious e tags. When replying to a kind 1111 comment,
the root event info is reconstructed from the comment's uppercase tags.
Added optional tags parameter to usePostComment so ComposeBox can pass
through hashtags, mentions, imeta, CW, and quote tags.
Previously, clicking Reply on non-kind-1 events (treasures, themes, vines,
etc.) would create a kind 1 event with NIP-10 tags. NIP-22 specifies that
comments on non-kind-1 events must use kind 1111 with proper uppercase
(root) and lowercase (parent) tag structure.
- Detect non-kind-1 reply targets and switch to kind 1111
- Build NIP-22 tags: A/E/I + K + P for root scope, a/e/i + k + p for parent
- Handle addressable, replaceable, and regular event kinds
- Copy uppercase root tags from kind 1111 targets when replying to comments
- Invalidate comments cache instead of replies cache for NIP-22 events
- Images now render inline via NoteContent at the position of their URL
rather than collected into a gallery block below the text
- Clicking an inline image opens a full lightbox with navigation across
all images in the post; lightbox index is tracked by position so
duplicate URLs open at the correct index
- Removed separate ImageGallery/extractImages from NoteCard,
PostDetailPage, and ComposeBox to prevent double-rendering
- Added disableEmbeds to NoteMoreMenu clamped preview to avoid block
embeds inside a line-clamp-3 container
- Fixed hasPreviewableContent in ComposeBox to detect image-only posts
so the Preview tab appears correctly
- Removed mp3/ogg/wav/pdf from MEDIA_URL_REGEX suppression list; those
URLs now render as clickable links instead of being silently dropped
- Added avif to IMAGE_URL_REGEX; added cursor-pointer to InlineImage
- Add useOpenPost hook to centralize left/middle-click navigation logic
- Apply middle-click (aux click) support to NoteCard, hot posts sidebar, and streams feed
- Fix ComposeBox textarea using break-all instead of break-words
After a file uploads, decode it on a small canvas (64px wide) to
compute the natural dimensions (dim) and a 4x3 blurhash. Both are
injected into the NIP-94 tag array before storing in uploadedFileGroups,
so the published imeta tag includes them automatically.
The cache-peek approach always returned npub because the cache was empty
at render time. Replace it with a proper hook that calls useNip05Verify,
relying on TanStack Query's deduplication to avoid redundant requests.
Also fix the dot separator in NoteCard to hide when NIP-05 is unverified,
and refactor ProfileSearchDropdown to pass the verified URL through
onClick rather than recomputing it in an event handler.
Replace the '?' avatar fallback and 'Anonymous' name placeholder with skeleton
loaders in ComposeBox and LeftSidebar during initial profile load. Expose
isLoading from useCurrentUser and useLoggedInAccounts to support this.
Type a colon followed by at least 2 characters (e.g. :fi) to trigger
an inline emoji autocomplete dropdown. Navigate with arrow keys, select
with Enter/Tab, or dismiss with Escape. Uses the existing emoji-mart
data for search by shortcode, name, and keywords.
Mentions now trigger the Edit/Preview toggle. Compose preview uses
NoteContent directly for link previews and embeds instead of rendering
them separately, matching how real notes display. LinkPreview falls back
to a domain+favicon card when OEmbed data is unavailable. Added
disableEmbeds prop to NoteContent for future use.