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.
Background images uploaded via BackgroundPicker are now resized to a
max of 1920px on the longest side and converted to JPEG (quality 0.85)
before uploading. Images that are already small JPEGs are passed through
unchanged. This reduces file size and improves load times when visiting
profiles with custom backgrounds.
New page at /badges/created lets users manage badge definitions they've
published. Features:
- Lists all kind 30009 events authored by the current user
- Edit: inline form to update name, description, and image (republishes
the addressable event with updated tags, preserving t/tier/price tags)
- Delete: NIP-09 deletion request with confirmation dialog
- Award: quick access to AwardBadgeDialog from each row
- View: link to the badge detail page (naddr)
- Back arrow to Badge Shop
Also added 'Created Badges' quick-action link on the Shop tab.
Both AchievementsPage and ShopPage now query badge definitions from
the BADGE_ACCOUNT_PUBKEY constant (npub10qdp2fc9ta6vraczxrcs8prqnv69fru2k6s2dj48gqjcylulmtjsg9arpj)
instead of config.nip85StatsPubkey.
Adds a check in useNostrPublish that compares the pubkey on the signed
event against the currently selected user's pubkey. This prevents
publishing events from a mismatched signer (e.g. a browser extension
or bunker configured for a different account).
Each badge award notification now shows an inline Accept button that
calls useAcceptBadge. Once accepted, it changes to a checkmark with
'Accepted' text. For grouped notifications, each award gets its own
accept button.
When replying to a Request to Vanish event, the reply composer now
shows a styled vanish preview instead of the generic author+content
card that looked broken for nuked accounts.
Register kind 62 in the EXTRA_KINDS feed system so Request to Vanish
events from followed users appear in the home feed. Enabled by default
via the feedIncludeVanish toggle, and can be disabled in feed settings.
When a Request to Vanish event is quoted/embedded inside another
post, it renders as a compact card with caution stripes, the glitch
icon, title, npub, and optional reason — matching the dramatic style
of the full vanish display.
Implement a visually striking display for kind 62 vanish events with
animated caution stripes, glitch effects, and a grand exit aesthetic.
Since the user has nuked their account, the npub is shown instead of
profile info. Supports both compact feed cards and full detail view
with comments and sharing.
Badge Shop (/shop) now has 3 tabs following the ThemesPage pattern:
- Shop: admin badge grid with categories, search, prices (unchanged)
- Follows: NoteCard feed of badge events from followed users
- Global: NoteCard feed of all badge events
The standalone BadgesFeedPage is deleted — its content now lives as
the Follows/Global tabs inside the Shop page.
- Add useBadgeFeed hook (modeled on useThemeFeed) for infinite scroll
- Remove 'badges' sidebar item, keep 'shop' and 'achievements'
- Add 'badges' → 'shop' sidebar ID migration so existing users
who had Badges in their sidebar seamlessly get Badge Shop instead
- Redirect /badges → /shop for backward compatibility
- Add quick-action links to Create Badge and My Badges on Shop tab
The activity indicator header above the post already shows the git
branch icon and 'shared a repository' text, making the in-card icon
redundant. Hashtags and the Public badge add noise without value.
Use a GitHub-inspired structure: repo icon + name (as primary-colored
link text) + Public/Fork pill badge on the first line, description on
the second line, and hashtags rendered as language-dot metadata in a
compact bottom row. Clone URL and action buttons follow below.
Wrap GitRepoCard, PatchCard, PullRequestCard, and CustomNipCard content
in rounded-2xl bordered containers matching the quote post / embedded
note visual pattern. Add KIND_HEADER_MAP entries for all four dev kinds
(30617, 1617, 1618, 30817) so the feed shows activity labels like
'shared a repository', 'submitted a patch', 'opened a pull request',
and 'proposed a NIP' above each post.
Achievements are now queried as kind 30009 badge definitions from the
admin pubkey with t:achievement tag, grouped by category t-tags.
Removed the static achievementCatalog.ts - the admin publishes all
achievement badges to relays and the client discovers them dynamically.
Add full badge system with achievements, shop, creation, and management:
- 11 new hooks for badge queries, mutations, and DVM claims
- 4 new pages: Achievements, Badge Shop, Badge Create, Badge Manage
- 4 new shared components: BadgeShowcaseGrid, BadgeThumbnail, BadgeTierPill, AwardBadgeDialog
- Badge showcase on ProfileCard and ProfileHoverCard
- Badge award notifications (kind 8) in notification feed
- Enhanced BadgeDetailContent with accept/award/buy actions
- Achievement catalog (73 achievements across 8 categories)
- Shop category system with admin-issued badge support
- Sidebar items for Achievements and Badge Shop
- CSS animations for badge glow and confetti effects
Built entirely on standard NIP-58 (kinds 30009, 8, 30008) with
NIP-90 DVM for achievement verification and NIP-57 zaps for shop.
Replace three separate per-hostname databases (nostr-nip05-cache-*,
nostr-profile-cache-*, nostr-dm-store-*) with one shared 'ditto'
database containing three object stores: nip05, profiles, messages.
A single db.ts module manages the connection and upgrade logic, and
caches the promise so only one connection is created per page lifetime.
Profile events (kind 0) are now persisted in IndexedDB and hydrated into
memory before React renders. useAuthor seeds TanStack Query with
initialData from the cache so names, avatars, and metadata render
instantly on repeat visits instead of showing skeletons.
Cache writes happen from three sites: useAuthor (single profile fetch),
useAuthors (batch fetch), and useFeed (community feed metadata prefetch).
Only newer events overwrite older ones to prevent out-of-order downgrades.
Staleness tiers match NIP-05: < 5 min fresh, 5 min - 7 d background
refetch, > 7 d expired (skeleton until fresh data arrives).
Successful NIP-05 resolutions are now persisted in IndexedDB with a
lastVerified timestamp. On subsequent page loads the cache is hydrated
into memory before React renders, so TanStack Query receives initialData
and skips the pending state entirely — verified NIP-05 names appear
instantly instead of showing a skeleton placeholder.
Failed lookups are not persisted. Stale entries are evicted when the
domain responds but the identifier is no longer present.
Make evolve process lighter and less repetitive:
- EVOLVE_REQUIRED_POSTS: 3 → 1
- Update task name: 'Create Posts' → 'Share Evolution'
- Update task description: 'Share 3 posts about evolving' → 'Post about your Blobbi evolving'
- Reduce query limit since only 1 post needed
Hatch process unchanged - still requires 3 posts with hatch-specific validation
- Add useActiveTaskProcess hook to consolidate hatch/evolve task logic
- Rename useSyncHatchTaskCompletions to useSyncTaskCompletions
- Fix badge to include dynamic tasks (was only counting persistent)
- Fix 'Edit Wall' task link to point to /settings/profile
- Reduce duplication in BlobbiPage.tsx by using unified hook
- Export new hook and types from blobbi/actions index
BUG 1 - task/task_completed tags not being written:
- incrementInteractionTaskTags now accepts requiredInteractions param
(7 for hatch, 21 for evolve) instead of hardcoded value
- useBlobbiDirectAction and useBlobbiUseInventoryItem now increment
interactions for BOTH incubating AND evolving states
- useSyncHatchTaskCompletions now syncs for both processes
BUG 2 - evolving missions not affecting badge:
- remainingTasksCount now uses active process tasks (hatch or evolve)
- allTasksComplete now checks both incubating and evolving states
- BlobbiBottomBar uses isInTaskProcess instead of isIncubating
Architecture rules preserved:
- Persistent tasks: can be cached in task/task_completed tags
- Dynamic tasks: NEVER stored in tags, UI-only
- No infinite loops, no retroactive increments
- Interactions only increment during real user actions
- Add min-height placeholder to InlineImage to prevent layout shifts when
images load from 0px to natural height
- Remove 60s refetchInterval from useFeed that caused all pages to re-sort
in the background, triggering full feed re-renders
- Pre-filter deprecated/hidden events at Feed level (shouldHideFeedEvent)
instead of returning null inside NoteCard after hooks have already run
- Stabilize author info skeleton/loaded height with consistent min-h-[42px]
to prevent cascading layout recalculations
Add comprehensive evolution task system parallel to incubation:
Architecture:
- Separate persistent tasks (event-based, cacheable in tags) from
dynamic tasks (stat-based, never cached, recomputed every render)
- Both hatch and evolve require all persistent AND dynamic tasks complete
New hooks:
- useEvolveTasks: 6 persistent tasks + 1 dynamic stat task (all stats >= 80)
- useStartEvolution/useStopEvolution: manage evolution state transitions
New components:
- TasksPanel: generalized task display for both hatch and evolve
- StartEvolutionDialog: confirmation dialog for starting evolution
- Updated BlobbiMissionsModal to handle both hatch and evolve flows
Evolve tasks (baby → adult):
- Create 3 themes, 3 color moments, 3 evolve posts
- 21 interactions, use Blobbi shape, edit wall once
- Dynamic: maintain all 5 stats >= 80
Key fixes:
- Renamed isEvolving variable collision in BlobbiPage
- Filter dynamic tasks from sync to prevent tag pollution
- Updated BlobbiPostModal to support evolve posts dynamically
Amber users on Android who manually approve events must switch from Ditto to
Amber to approve, then switch back. Backgrounding Ditto can freeze its
WebSocket, causing the NIP-46 response to be silently dropped — leaving the
operation hanging with no feedback and no way out. Users with Amber
notifications working correctly are unaffected, as approving via notification
does not background Ditto.
Even with auto-approve enabled, kinds outside Amber's default whitelist
require manual approval. Ditto uses several of these regularly: kind 1059
(NIP-17 gift-wrap DMs), 1111 (comments), 1311 (live chat), 31925 (RSVPs),
24242 (Blossom file upload auth), and 30078 (app settings).
This introduces signerWithNudge, a NostrSigner wrapper with the following
behaviour:
Nudge toast after 4 seconds
If a signing or encryption op is still pending after 4 s, a persistent toast
appears naming what is being approved (e.g. 'Approve file upload auth'), with
a human-readable label derived from the event kind.
Android 'Approve in signer' button
On Android the nudge toast includes an 'Approve in signer' button that opens
Amber via the nostrsigner: URI scheme, keeping the WebSocket alive. After
tapping, the button becomes a spinner and a Cancel button appears.
Automatic retry on foreground resume
When Ditto returns to the foreground after being backgrounded, it
automatically retries the pending NIP-46 request (up to 2 times) and shows a
brief 'Checking for signer response' toast.
Hard 45-second timeout
Operations with no response within 45 s are rejected with a clear error.
Cancel / Skip
The nudge toast has a Skip link throughout. After tapping 'Approve in signer'
it becomes a full Cancel button.
Multi-phase encrypt-then-sign
Saving app settings (kind 30078) and mute lists (kind 10000) require a nip44
encrypt followed immediately by a signEvent. When the encrypt nudge was shown,
a phase-transition toast tells the user a second approval is coming. The check
is kind-specific to avoid false positives.
Success feedback
A brief 'Approved' toast confirms the outcome when the nudge was shown.
Relay connectivity check
At nudge time, if the bunker relay WebSocket is not OPEN the toast warns
'Signer relay unreachable' instead of prompting for an approval that cannot
be delivered.
Accessibility
Toast buttons meet the 44 px touch target minimum. Text size and contrast
were increased for readability on small screens.
- remainingTasksCount now depends on hatchTasks.tasks (not just completedTaskIds)
to correctly reflect loading state and task changes
- allTasksComplete is now a memoized value that prevents false positives by
checking: isIncubating && !isLoading && tasks.length > 0 && remaining === 0
- Changed missions badge symbol from '?' to '!' when all tasks complete
Task Sync Stability:
- Remove hatchTasks.tasks from useEffect dependencies (was causing instability)
- Derive tasksToSync and remainingTasksCount via useMemo keyed off completedTaskIds
- Effect dependencies now only include stable primitives: completedTaskIds,
cachedCompletedIds, hatchTasks.isLoading, companion?.state
Content Fix on Stage Transition:
- Add generateBlobbiContent() helper with correct grammar ('an egg' vs 'a baby')
- useBlobbiHatch now generates new content: '{name} is a baby Blobbi.'
- useBlobbiEvolve now generates new content: '{name} is an adult Blobbi.'
- Content always reflects current stage (was keeping old egg content)
Missions Icon Badge:
- Show remaining task count when incubating with tasks remaining
- Show '?' badge (success variant) when all tasks complete during incubation
- Badge variants: default (blue), warning (amber), success (emerald)
Blobbies Icon Badge:
- Now shows count of Blobbies needing care (any stat < CARE_THRESHOLD=40)
- Warning variant (amber) when there are needy Blobbies
- Only shows badge when count > 0
Selector Modal Warning:
- Add AlertTriangle indicator in top-right corner for Blobbies needing care
- Uses same companionNeedsCare() logic as bottom bar badge
Infinite Loop Fix:
- Add useRef anti-loop memory (lastSyncedKeyRef) to track last synced key
- Mark key as synced BEFORE calling sync to prevent race conditions
- Add guard for companion.state !== 'incubating'
- Remove syncTaskCompletions from useEffect deps (intentional, prevents
re-triggering when mutation function reference changes)
- Reset ref on error to allow retry
Missions Badge:
- Add remainingTasksCount prop to BlobbiBottomBar
- Show badge on Missions button when incubating with incomplete tasks
- Update BottomBarButton to show badge when count > 0 (was > 1)
The interaction task tags were already being updated correctly by
incrementInteractionTaskTags() in useBlobbiDirectAction and
useBlobbiUseInventoryItem hooks during real user interactions.
- useStartIncubation now requires explicit mode ('start', 'restart', 'switch')
instead of auto-detecting behavior. This makes the flow predictable.
- StartIncubationDialog determines mode and passes it to onConfirm callback
- Removed useUpdateTaskProgress hook (architecturally inconsistent - updated
last_interaction during cache-only sync, violating the rule that only real
user actions should update timestamps)
- BlobbiPostModal now requires blobbiName and process props for stage-aware
post generation with Blobbi name as first hashtag
- isValidBlobbiPost() now validates the Blobbi name hashtag is present
- Added sanitizeToHashtag helper to both BlobbiPostModal and useHatchTasks
for consistent hashtag generation
Audit and hardening of hatch task cache sync:
1. BlobbiPage useEffect:
- Use useMemo to create stable string keys for completion comparison
- Only trigger sync when computed completions differ from cached
- Skip sync entirely if no diff exists
- Add dev-only debug logs
2. useSyncHatchTaskCompletions:
- Remove last_interaction update (this is cache sync, not user action)
- Add double diff check: first against companion.tasksCompleted, then against canonical.allTags
- Return detailed result with skip reasons for debugging
- Add dev-only debug logs for all sync decisions
3. incrementInteractionTaskTags:
- Add check for already-completed state to prevent duplicate task_completed tags
- Return previousCount for debugging
- Add dev-only debug logs
- Document that this is NOT idempotent by design (each call = real interaction)
Key guarantees:
- Cache-only sync never mutates last_interaction
- Multiple renders with same data = no publish
- WebSocket updates or refetches = no publish unless real diff
- No duplicate task_completed tags possible
Phase 2 of Blobbi incubation audit:
- Add BlobbiMissionsModal component with HatchTasksPanel integration
- Move hatch tasks UI from main page to Missions modal
- Add useStopIncubation hook with confirmation dialog
- Enforce only one Blobbi incubating at a time (auto-stops previous)
- Enhance StartIncubationDialog to show switch warning for other incubating Blobbi
- Add useSyncHatchTaskCompletions hook to sync task completions to kind 31124 tags
- Consolidate STAT_MIN/STAT_MAX to single source in src/lib/blobbi.ts
- Remove unused BlobbiPlaceholderModal component
- Remove auto-save effect that was overwriting user selection during
WebSocket/query updates. User selection now only persists via explicit
handleSelectBlobbi() call.
- Add debug logging to trace selection changes in development mode.
- Add STAT_MIN=1 and STAT_MAX=100 constants to blobbi-decay.ts and update
clamp() to use STAT_MIN instead of 0, preventing soft-lock issues.
- Fix buildMigrationTags() to always derive and include all visual traits
during migration, ensuring every migrated event has complete visual data.
Display NIP-85 follower count next to the existing following count on
profiles. Clicking the count opens a paginated modal that fetches
followers via kind:3 #p queries with infinite scroll.