Commit Graph

4447 Commits

Author SHA1 Message Date
filemon ea8d3dd0f3 Extract real implementations into mouth/, eyebrows/, bodyEffects/ modules
Complete the modularization of the visual emotion system. Each subsystem
now owns its implementation rather than re-exporting from emotions.ts.

mouth/ (554 lines):
- detection.ts: marker-based + regex fallback mouth detection, replacement
- generators.ts: round, sad, droopy, big smile, small smile, drool, food
  icon, sleepy mouth morph animation
- types.ts: MouthPosition, MouthDetectionResult, shape configs
- index.ts: barrel exports

eyebrows/ (194 lines):
- generators.ts: eyebrow generation with per-eye overrides,
  variant/form offsets, animated bounce styles
- types.ts: EyebrowConfig, AnimatedEyebrowsConfig
- index.ts: barrel exports

bodyEffects/ (392 lines):
- generators.ts: body path detection, dirt marks, stink clouds,
  anger-rise effect (full implementation moved from emotions.ts)
- apply.ts: applyBodyEffects() composable applicator
- types.ts: BodyEffectConfig, BodyPathInfo, BodyEffectsSpec
- index.ts: barrel exports

eyes/ (unchanged):
- Already correctly modular. Confirmed no stranded eye code remains
  in emotions.ts — the legacy applySadEyeWaterFill/applySadEyeHighlights
  functions were dead code (superseded by eyes/effects.ts) and removed.

emotions.ts (2135 → 628 lines):
- Now a pure orchestrator: recipe definitions + composition logic
- Imports all implementations from subsystem modules
- Contains only: BlobbiEmotion type, EMOTION_CONFIGS recipes,
  applyEmotion() orchestrator, tear generation (cross-cutting overlay),
  sleepy animation coordination (cross-cutting, touches eyes + mouth),
  and deprecated re-exports for backward compatibility
- No more detection internals, SVG geometry generators, or effect
  implementations
2026-03-29 19:55:27 -03:00
Alex Gleason 02231ea1f9 Fix avatar shape flash by computing mask URL synchronously
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.
2026-03-29 17:47:57 -05:00
Alex Gleason effc704613 Fix 'Cannot update component while rendering' warning in useLayoutOptions
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.
2026-03-29 17:43:55 -05:00
Alex Gleason 51fb1fd1cb Give comments (kind 1111) and generic reposts (kind 16) independent feed toggles
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.
2026-03-29 17:33:56 -05:00
Alex Gleason 1988e1b849 Fix duplicate React keys in content settings
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.
2026-03-29 17:29:45 -05:00
filemon cf0524a211 Extract composable visual modules and decouple dirty from face emotions
Foundation for migrating the monolithic emotion system toward a composable
architecture where each visual area (eyes, mouth, eyebrows, body effects)
is handled independently.

New modules created:
- bodyEffects/ — types, generators (dirt marks, stink clouds, anger rise),
  and applyBodyEffects() for applying body decorators independently of face
- mouth/ — types and re-exports of existing mouth detection/generation
- eyebrows/ — types and re-exports of existing eyebrow generation

Dirty emotion refactored:
- Removed face modifications (droopyMouth, eyebrows) from EMOTION_CONFIGS.dirty
- dirty is now a body-only decorator that adds dirt marks + stink clouds
  without touching eyes, mouth, or eyebrows
- Hygiene stat now maps to 'boring' as the face emotion (same as happiness)
- Body effects (dirty) are resolved independently in resolveStatusEmotions()
  and flow as a separate bodyEffects field through the entire pipeline:
  resolveStatusEmotions → useStatusReaction → BlobbiStageVisual →
  BlobbiAdultVisual/BlobbiBabyVisual → applyBodyEffects()
- Any face + dirty is now possible: boring+dirty, sleepy+dirty, dizzy+dirty

The existing emotion system (applyEmotion) still works unchanged for all
other emotions. The eyes/ module already existed. This is an incremental
step — no full migration yet.
2026-03-29 17:57:45 -03:00
filemon 8d04bbbdbe Complete baseEmotion + overlayEmotion migration across the visual pipeline
Finish the two-layer emotion architecture so resolveStatusEmotions() is
the single source of truth and both the main BlobbiPage and the floating
companion use the same flow.

useStatusReaction:
- Now returns baseEmotion, overlayEmotion, triggeringBaseStat,
  triggeringOverlayStat, isStatusReactionActive, currentSeverity,
  isOverrideActive (replaces the old single currentEmotion).
- Internally calls resolveStatusEmotions() on every check cycle and
  tracks base and overlay transitions independently with animation
  safety per layer.
- Action overrides replace the overlay; the base persists underneath.

status-reactions.ts:
- Remove combineEmotions() (no longer needed).
- Deprecate resolveStatusReaction() with a JSDoc notice.
- resolveStatusEmotions() is now the primary API.

BlobbiStageVisual:
- Accepts a new baseEmotion prop and forwards it to BlobbiBabyVisual
  and BlobbiAdultVisual.

BlobbiPage (main consumer):
- Destructures baseEmotion + overlayEmotion from the hook and passes
  both through to BlobbiStageVisual.

Companion system:
- CompanionData now carries full BlobbiStats and state.
- BlobbiCompanionLayer runs its own useStatusReaction to drive the
  companion's emotions from stats, including item-use action overrides.
- BlobbiCompanion and BlobbiCompanionVisual accept baseEmotion + emotion
  props and forward them to the underlying visual components.
2026-03-29 17:27:17 -03:00
Alex Gleason f6c08f8afa Add badge list recovery dialog for kind 10008 events
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.
2026-03-29 15:22:51 -05:00
Alex Gleason 3197c53fcc Fix PageSkeleton to match actual page layout structure
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).
2026-03-29 15:14:09 -05:00
Alex Gleason 7b793149b3 Reserve grid cell for +N overflow indicator in badge lists
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.
2026-03-29 15:08:48 -05:00
Alex Gleason 24c938728a Replace spinner with skeleton grid for badge list loading state
Shows placeholder skeletons matching the badge grid layout (48px
rounded squares + name bars) instead of a centered spinner while
badge definitions are being fetched.
2026-03-29 15:06:33 -05:00
Alex Gleason 1c358a3c79 Add compact badge row preview for embedded profile badges events
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).
2026-03-29 15:04:58 -05:00
Alex Gleason 3bbed8875c Remove inline compose box from profile badges detail view
The badge collection detail page (kind 10008/30008) no longer shows
a ComposeBox between the badge grid and the comments section.
2026-03-29 15:02:12 -05:00
filemon a3e6ff34db Add boring and dirty emotions to dev emotion tester panel
- Added boring emotion (😑) - low-energy, unamused expression
- Added dirty emotion (💩) - hygiene-specific with dirt/stink visuals
- Maintains existing emotion order with new emotions near the top
2026-03-29 17:01:48 -03:00
Alex Gleason 0f7fc673eb Make +N overflow indicator clickable to expand full badge grid
Clicking the '+2' (or similar) button at the end of a truncated badge
grid now reveals all badges instead of being a static indicator.
2026-03-29 15:01:17 -05:00
Alex Gleason 646c95a86f Show kind action header on threaded ancestor NoteCards
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.
2026-03-29 14:56:55 -05:00
Alex Gleason 87a8974c8c Show parent event as threaded NoteCard for kind 1111 comments
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.
2026-03-29 14:55:48 -05:00
Alex Gleason 9b5df28b93 Fix naddr routing for replaceable events (kind 10000-19999)
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.
2026-03-29 14:47:52 -05:00
Alex Gleason e876e290da Encode replaceable events (kind 10000-19999) as naddr URLs
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.
2026-03-29 14:45:45 -05:00
Alex Gleason f26f033b14 Support viewing kind 10008/30008 profile badges via nevent URLs
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).
2026-03-29 14:41:24 -05:00
Alex Gleason 565f323179 Add mouse-only 3D tilt to BadgeThumbnail with visible effect
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.
2026-03-29 14:37:46 -05:00
filemon 82b2aeb294 Refactor Blobbi visual state system: separate base face from overlay animations
- Add 'boring' persistent face: low-energy, unamused expression (replaces sad as generic fallback)
  - Droopy mouth with shallow curve, flat eyebrows
  - Used for non-critical bad stats (health, happiness)

- Add 'dirty' persistent state: hygiene-specific visuals
  - Includes dirt marks on lower body (3 curved scratch-like lines)
  - Animated stink clouds floating upward
  - Uses boring face as base + hygiene effects

- Refactor 'sleepy' to be an overlay animation
  - KEY FIX: sleepy now animates the CURRENT mouth state instead of resetting to default smile
  - When Blobbi is unwell (boring/dirty/dizzy face), sleepy animation preserves that base face
  - Implementation: applySleepyMouthAnimation finds existing mouth path and animates from there
  - Example: boring face + sleepy = boring expression with sleepy animation on top

- Update status-reactions.ts emotion mapping
  - health: boring (not feeling good) → dizzy (critical)
  - hygiene: dirty (poor hygiene visuals)
  - happiness: boring (low energy, unamused)
  - energy: sleepy (now an overlay, not base-replacing)

- Add base + overlay emotion architecture to visual components
  - BlobbiAdultVisual and BlobbiBabyVisual now accept optional baseEmotion prop
  - Emotions applied sequentially: base first, then overlay
  - Preserves existing behavior when only one emotion provided

- Add resolveStatusEmotions() utility
  - Separates base emotions from overlay emotions
  - Returns StatusEmotionResult with baseEmotion and overlayEmotion
  - Enables proper multi-stat handling (e.g., low health + low energy = boring face with sleepy overlay)

Architecture notes:
- Base emotions (boring, dirty, dizzy, sad, happy, etc.): replace face completely
- Overlay emotions (sleepy): animate on top without replacing base
- Critical fix: Blobbi no longer visually resets to happy during sleepy cycle when in bad state
2026-03-29 16:36:16 -03:00
Alex Gleason 6b59658f00 Add mouse-only 3D tilt effect to badge images in feed cards
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.
2026-03-29 14:28:33 -05:00
Alex Gleason 2e1e4416b3 Add touch support to badge 3D tilt effect
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.
2026-03-29 14:26:02 -05:00
Alex Gleason e92a2c571c Remove 'Badge' pill from badge detail issuer row
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.
2026-03-29 14:20:52 -05:00
Alex Gleason d3a418b5ee Remove redundant header from profile badges feed card
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.
2026-03-29 14:18:37 -05:00
Alex Gleason c9945107e9 Replace Photos and Videos with Badges in default sidebar order 2026-03-29 14:14:55 -05:00
Alex Gleason 0df942cb9d Display kind 20 photo events in detail view and add Photo indicator
- 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
2026-03-29 14:09:39 -05:00
Alex Gleason 37a63f068b Make version tag and date clickable links, remove external link icon 2026-03-29 13:48:07 -05:00
Alex Gleason cc8638e8b2 Show build date instead of commit hash in pre-release banner, compare to current commit 2026-03-29 13:46:26 -05:00
filemon fd20081ce8 fix: prevent reaction animations from being cut off by status recomputation
Refactored useStatusReaction hook to be more stateful and animation-aware:

- Track currently active reaction to avoid restarting same reaction
- Distinguish between persistent (sleepy, sad, dizzy, hungry) and one-shot reactions
- Persistent reactions loop continuously while condition remains active
- Only replace reactions when: type changes, higher priority interrupts, or one-shot completes
- Remove stats from useEffect dependencies to prevent reset on every recomputation
- Add animation cycle duration awareness to avoid mid-animation interruptions
- Use refs for stats/timing to maintain stable callback references

This ensures:
- Sleepy animation completes full cycle including slow eye opening
- Crying/sad reactions don't reset before tear cycle completes
- Dizzy animation doesn't keep resetting its visual motion
- Eyebrow/face reactions don't flicker from repeated reapplication
2026-03-29 15:17:33 -03:00
Alex Gleason cf9d409166 Migrate profile badges from kind 30008 to kind 10008
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.
2026-03-29 13:10:22 -05:00
Chad Curtis dc3be6564b Fix stale-cache overwrites in replaceable event mutations
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.
2026-03-29 10:05:58 -05:00
Chad Curtis 337e27f2b5 Add multi-select badge awarding with already-sent indicators 2026-03-29 09:47:05 -05:00
Chad Curtis c400437662 Add kinds 10015, 10030, 10063 to NostrBatcher REPLACEABLE_KINDS
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.
2026-03-29 08:51:41 -05:00
Chad Curtis e8941e8ef6 Fix search page 'N new posts' pill showing unfiltered count
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.
2026-03-29 08:00:29 -05:00
Chad Curtis 169823980f Add arc overhang spacer to Photos and Videos pages
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.
2026-03-29 07:58:26 -05:00
Chad Curtis aa7376b357 Add pull-to-refresh to all feed pages via usePageRefresh hook
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
2026-03-29 06:54:58 -05:00
Chad Curtis 6a0e88cbf1 Fix gap between arc and top nav on relay feed page mobile 2026-03-29 06:36:14 -05:00
Chad Curtis 01b7e1cea2 Stop click propagation on delete confirmation AlertDialog content
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.
2026-03-29 06:23:01 -05:00
Chad Curtis 910f43e0a5 Stop click propagation in NoteMoreMenu items to prevent click-through
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.
2026-03-29 06:21:15 -05:00
Chad Curtis 6bf630bb40 Fix delete post dialog freezing feed on desktop
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.
2026-03-29 06:16:23 -05:00
filemon 5ffab157d7 Unify eye system contract and fix animation conflicts
- Standardize data attributes: data-cx/cy → data-eye-cx/cy with legacy fallback
- Replace hardcoded eye selectors with EYE_CLASSES constants from eyes/types
- Remove unused side-specific clip rect class variants from EYE_CLASSES
- Fix sleepy animation: skip JS blink when SMIL animations present
- Fix companion reaction support: skip eye transforms when CSS animations active
- Update detection.ts to try new attribute format first, fall back to legacy
- Update useBlobbiEyes and useExternalEyeOffset to respect CSS animations
2026-03-29 05:16:51 -03:00
filemon c6e791d18f Implement reactions 2026-03-29 04:41:42 -03:00
Lemon a80b306248 Reset feed composer to collapsed state after posting 2026-03-28 23:11:47 -07:00
Lemon c8c294a8ad Match ComposeBox background opacity with header and subheader (bg-background/85) 2026-03-28 23:11:47 -07:00
Lemon 5a30376f2c Fix emoji shortcode autocomplete text and highlight colors
The shortcode label used text-muted-foreground making it hard to read.
Changed to inherit text-popover-foreground so it matches the theme's
normal text color. The selection/hover highlight now uses bg-secondary/60,
matching the subtle hover shade used by the NoteMoreMenu items.
2026-03-28 23:07:21 -07:00
Lemon 373219ecfa Reorder emoji picker: Recent, Custom, then standard categories
Move the Custom (NIP-30) emoji tab from last position to second, right
after Recent, so users can quickly access their custom emoji packs.
2026-03-28 23:07:21 -07:00
Lemon 1ef1400699 Track custom emoji usage so they appear in quick-react bar
Custom emojis were explicitly excluded from usage tracking, so they could
never accumulate enough count to appear in the quick-react popover. The
bar already handles displaying and filtering custom emojis correctly
(including removing stale ones from deleted packs), so the only missing
piece was the tracking call.
2026-03-28 23:07:21 -07:00
Lemon 7966d07158 Fix custom emoji SVGs not rendering in emoji-mart picker
emoji-mart renders custom emoji <img> tags with only max-width/max-height
inline styles but no explicit width/height. SVG files that lack intrinsic
width/height attributes (even with a viewBox) collapse to 0x0 because
max-* constraints alone can't force dimensions on a dimensionless image.

Inject a CSS rule into emoji-mart's shadow DOM that gives custom emoji
images explicit 1em x 1em dimensions with object-fit: contain. The img
lives inside span.emoji-mart-emoji, not a button with data-emoji-set.
2026-03-28 23:07:21 -07:00