Commit Graph

3308 Commits

Author SHA1 Message Date
sam a56860a6ce logo/copy changes 2026-04-17 12:15:51 +05:45
sam 9550094ffb wip mega dump/migration from ditto 2026-04-17 12:10:11 +05:45
Alex Gleason 71918f8381 release: v2.8.0 v2.8.0 2026-04-16 18:05:50 -05:00
Alex Gleason 99fefdda67 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-16 17:17:47 -05:00
Alex Gleason dabe3c1687 Fix avatar shape not saving during signup
The signup and onboarding profile steps rendered ProfileCard without
passing onAvatarShape, so emoji shape selections were silent no-ops and
never made it into the published kind 0 event.
2026-04-16 17:06:27 -05:00
Chad Curtis 1caf911f53 Merge branch 'ai-chat-429' into 'main'
Overhaul AI chat: handle 429 rate limiting, require Shakespeare credits

Closes #230

See merge request soapbox-pub/ditto!187
2026-04-16 22:03:57 +00:00
Chad Curtis c3f0e9d3fa Merge branch 'main' of gitlab.com:soapbox-pub/ditto into ai-chat-429
# Conflicts:
#	package-lock.json
2026-04-16 16:59:54 -05:00
Chad Curtis bc39c99d07 Merge branch 'feat/evolution-missions-to-kind-11125' into 'main'
Move hatch/evolve task progress from kind 31124 tags to kind 11125 evolution[]

Closes #234

See merge request soapbox-pub/ditto!186
2026-04-16 21:41:51 +00:00
Chad Curtis 377b536456 Merge remote-tracking branch 'origin/main' into feat/evolution-missions-to-kind-11125
# Conflicts:
#	package-lock.json
2026-04-16 16:35:38 -05:00
Chad Curtis bf0fde9d06 Fix interaction tally not incrementing: ensure evolution missions exist in session store
The interactions tally mission was silently dropped because
trackEvolutionTally maps over the evolution[] array — if it's empty,
nothing gets incremented. This happened when evolution missions
weren't persisted to kind 11125 or weren't hydrated on page load.

Both useHatchTasks and useEvolveTasks now have a safety-net effect:
if the companion is in an active task process (incubating/evolving)
but evolution[] is empty, they re-populate from the static mission
definitions. This ensures tally tracking works immediately regardless
of hydration timing.
2026-04-16 16:33:45 -05:00
Alex Gleason fb5278b891 Add nsec backup to Profile settings
Lets users with a local-nsec login reveal, copy, and back up their secret
key from /settings/profile. Uses saveNsec() so iOS gets iCloud Keychain,
Android gets Credential Manager with a file fallback, and web gets a
.nsec.txt download plus an opportunistic PasswordCredential save.

Renders an explanatory message for NIP-07 extension and NIP-46 bunker
logins, where the key is not accessible from the app.
2026-04-16 16:24:07 -05:00
Chad Curtis a27ee3af86 Fix self-review findings: remove dead code, fix task progress display, fix hydration race
- Remove dead code: useSyncTaskCompletions, incrementInteractionTaskTags,
  getInteractionCount, getEvolveInteractionCount, unused lookup maps
- Fix task progress showing 0/N on load: compute event-based task counts
  directly from Nostr query results (authoritative) instead of relying
  solely on the evolution mission store which may not be hydrated yet.
  Use max(queryCount, missionCount) so progress displays immediately.
- Fix hydration race: useDailyMissions raw memo now waits for hydration
  before creating fresh missions, preventing overwrite of persisted
  evolution[] with empty array. Also preserve evolution missions across
  daily resets during hydration.
- Fix session store miss: use ensureSessionStore in incubation/evolution
  start so evolution missions are always populated even if the store
  hasn't been hydrated yet.
- Extract duplicate findMission to shared findEvolutionMission in
  evolution-missions.ts
- Document evolution[] field on kind 11125 in NIP.md
2026-04-16 16:17:57 -05:00
Alex Gleason 7073cadb43 release: v2.7.1 v2.7.1 2026-04-16 16:09:12 -05:00
Alex Gleason 2dfb880566 Improve signup save-key step
Addresses confusion on the key-save step during signup:

- Rename the primary button from 'Continue' to 'Save Key' with a
  Download icon, so the label matches the action it performs.
- Change saveNsec() to return 'saved' | 'saved-to-file' | 'dismissed'
  instead of throwing on native dismissal. Dismissing the iCloud
  Keychain prompt is a legitimate user choice so the handler now
  proceeds silently rather than blocking with a 'Save failed' toast.
- Add an in-flight guard on the Save Key button with a spinner and
  'Saving…' label. The finally block guarantees the disabled state is
  cleared, so users can never get stuck on an unresponsive button —
  fixing the 'button became disabled after I dismissed the prompt'
  complaint by construction.
- On de-Googled Android builds (GrapheneOS, /e/OS, etc.) the AndroidX
  Credential Manager has no provider to delegate to, so the keychain
  save fails immediately. Fall back to writing the key to the app's
  Documents directory so the user always has a persistent backup, and
  surface a toast telling them where the file is.
- iOS keeps its original behaviour: dismissing the iCloud Keychain
  sheet is a deliberate user choice, no automatic fallback. The
  Documents folder on iOS is accessible via the Files app without
  authentication, so silently dropping a plaintext nsec there would
  violate user intent.
- Use the app name (from config.appName) as the filename slug for any
  .nsec.txt file written to disk. On Capacitor location.hostname is
  always 'localhost', so passing the app name is the only way to get
  a meaningful filename. Drop the redundant 'nostr-' prefix since the
  '.nsec.txt' extension already identifies the file.
- Rewrite the description and title on the save step: 'Your secret
  key' + a single paragraph explaining what the key is and why it
  matters.
- When the user reveals the key via the eye toggle, show an amber
  callout with sharing/screenshotting warnings and a 'Learn more' link
  to the Managing Nostr keys blog post. The warning appears at the
  moment risk is highest.
- Auto-select the full nsec on focus/click so users copying into a
  password manager don't have to fight mobile selection handles.
- Use openUrl() for the external 'Learn more' link so it works
  correctly inside Capacitor's WKWebView.
- Singularise the keygen step copy ('cryptographic key' / 'Generate
  my key') to stay consistent with the save step which presents a
  single secret key.
2026-04-16 15:50:59 -05:00
Chad Curtis 13d4f667b6 Restore AI chat widget and extract shared credits hook
- Restore full interactive chat widget with ScrollArea, streaming messages,
  input area, and conversation cache that was regressed in ec9b6c43
- Extract useShakespeareCredits hook so credits gating is DRY between the
  widget and the full AI chat page
- Show Dork ASCII mascot consistently across all empty/logged-out states
  instead of the generic Bot icon
2026-04-16 15:47:33 -05:00
Chad Curtis d73460a617 Fix self-review findings: invalid HTML nesting, credits error handling, swallowed 400 errors 2026-04-16 15:36:09 -05:00
Chad Curtis ec9b6c43be Overhaul AI chat: handle 429 rate limiting, require Shakespeare credits
- Add RateLimitError class with Retry-After header parsing
- Distinguish insufficient_quota 429 from rate-limit 429
- Friendly Dork-themed error banners for rate limiting and out-of-credits
- Clean no-credits empty state with directive CTA and Get Credits button
- Hide model selector, trash, and input when user has no credits
- Hide page title on mobile, align model selector right
- Simplify sidebar widget to Shakespeare CTA
2026-04-16 15:28:09 -05:00
Alex Gleason 0d3b8ed23d Harden CSS/URL handling, NWC storage, and Android backup
- Sanitize event-sourced URLs before CSS url() interpolation in
  ProfileCard banner and letter stationery background (closes H-1, H-2)
- Sanitize event-sourced font families at the parse layer and in letter
  card/detail consumers that bypass resolveStationery (closes M-6)
- Export sanitizeCssString for broader reuse
- Route NWC wallet connection URIs and active pointer through a new
  useSecureLocalStorage hook, storing in iOS Keychain / Android KeyStore
  on native (closes M-1)
- Add removeItem to secureStorage
- Add Android backup/data-extraction rules that exclude WebView storage
  and Capacitor secure-storage SharedPreferences so wallet credentials
  don't leak via Google Auto Backup (closes M-5)
- Document that GOOGLE_PLAY_SERVICE_ACCOUNT_JSON must be base64-encoded
  to match what the CI job expects (closes M-2)
2026-04-16 14:20:26 -05:00
Alex Gleason a61925b821 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-16 13:50:42 -05:00
Alex Gleason cbfbca063e Validate theme font and background URLs at the schema layer
`ThemeFontSchema.url` and `ThemeBackgroundSchema.url` previously accepted
any string, relying entirely on downstream `sanitizeUrl()` calls for
protocol enforcement. Tightening the schema to `z.url()` rejects
obviously malformed inputs up front and matches the approach already used
for the relay list (`BlossomServersEventSchema`). `sanitizeUrl()` remains
the authoritative guard for `https:` enforcement at render time.
2026-04-16 13:47:16 -05:00
Alex Gleason f3393b2cc8 Store nostr-push device key in secure storage on native builds
The per-device ephemeral key used to sign nostr-push RPC events was
previously stored unconditionally in localStorage. On Capacitor builds
this bypassed the iOS Keychain / Android KeyStore wrapper that every
other persistent key in the app already uses.

Route the key through `secureStorage`, which keeps the native path
encrypted at rest and falls back to localStorage on web (where it was
before). Because the key is now loaded asynchronously, convert the
`NostrPushClient` constructor into a private constructor plus a public
`create()` factory, and restructure `usePushNotifications` bring-up to
await the client before registering the service worker.

The key is ephemeral and per-device, so compromise only reveals which
Nostr events this device subscribes to -- not the user's identity --
but matching the existing secure-storage contract closes an obvious
inconsistency.
2026-04-16 13:47:16 -05:00
Alex Gleason 2eb643f422 Sanitize app-handler picture and banner URLs
The `picture` and `banner` fields parsed from a kind 31990 NIP-89 event's
JSON content were passed directly to `<img src>` attributes without any
scheme validation. Non-https URLs could leak the user's IP to arbitrary
hosts, and data: URIs could be used for fingerprinting.

The same event's `website` URL was already sanitized; apply the same
treatment to the image URLs for consistency. The app's CSP `img-src`
already blocks most of these at the browser level, so this is
defense-in-depth.
2026-04-16 13:47:15 -05:00
Alex Gleason e22dbbe85c Validate NIP-05 resolver returns a 64-char hex pubkey
Previously the resolver accepted any string value from a domain's
.well-known/nostr.json `names` map and persisted it to IndexedDB. A
malicious or misconfigured NIP-05 server could return arbitrary data
(non-hex, wrong length, HTML, etc.) that would then be cached and
passed to downstream consumers as a pubkey.

Exploitation impact is limited because invalid hex simply fails to
match anywhere in the Nostr filter API, but hygiene and cache
integrity warrant rejecting malformed values outright. Enforce the
standard 64-char lowercase hex shape and evict any cached entry that
fails validation.
2026-04-16 13:47:15 -05:00
Alex Gleason e01ed039fb Add restrictive sandbox attribute to web sandbox iframe
Previously the SandboxFrame iframe relied entirely on cross-origin
subdomain isolation (the HMAC-derived `<id>.sandbox.ditto.pub` origin)
for containment. That does give origin-keyed storage and postMessage
isolation, but it does not restrict top-frame navigation, pointer lock,
or other capabilities that a hostile nsite/webxdc app could abuse.

The highest-value protection here is blocking `allow-top-navigation`:
without it, a malicious nsite could do `window.top.location = evilUrl`
and redirect the entire Ditto tab to a phishing page that impersonates
the app. The user opened a preview expecting to stay inside Ditto, so
this is a realistic and impactful attack.

The policy grants the capabilities that real web apps legitimately use
(scripts, same-origin storage + Service Workers per iframe.diy's
architecture, forms, modals, popups that escape the sandbox, downloads)
while withholding the ones that are either attacks (top navigation) or
unused niche features (pointer lock, presentation API, orientation
lock).

Also Omit 'sandbox' from the spread props so consumers cannot
accidentally weaken the policy.
2026-04-16 13:47:13 -05:00
Chad Curtis 17cdb87723 Merge branch 'fix/scroll-restoration-on-back-navigation' into 'main'
Fix scroll position lost when navigating back from post detail page

Closes #217

See merge request soapbox-pub/ditto!161
2026-04-16 18:35:28 +00:00
Alex Gleason a55ff61669 Verify NIP-17 inner rumor pubkey matches seal pubkey
NIP-17 requires that clients verify `messageEvent.pubkey === sealEvent.pubkey`
before trusting a gift-wrapped direct message. Without this check, any
attacker can construct a rumor claiming to be from another user and
gift-wrap it to the victim -- the seal signature only authenticates the
seal author, not the (unsigned) inner rumor.

Ditto's primary sender display uses sealEvent.pubkey so the headline
impersonation case is mitigated in practice, but the inner event's fields
(including its pubkey) are passed whole to NoteContent for kind 15 file
attachments, which could leak into downstream zap/reply targeting. Add
the spec-mandated check to prevent any trust in the inner pubkey.
2026-04-16 13:21:15 -05:00
Chad Curtis 5c215aeec5 Add debounced persistence for evolution mission progress
The in-memory session store doesn't survive page refresh. Add
usePersistEvolutionProgress hook that listens for evolution mission
changes and debounce-publishes (5s) to kind 11125 content JSON via
fetchFreshEvent + serializeProfileContent. Wired into BlobbiPage.
2026-04-16 13:11:41 -05:00
Chad Curtis 591ab57352 Fix lint: remove unused imports, wrap evolution in useMemo 2026-04-16 12:06:22 -05:00
Chad Curtis cb42b1b6a3 Move hatch/evolve task progress from kind 31124 tags to kind 11125 evolution[]
Migrate the hatch/evolve task system to use MissionsContent.evolution[]
on kind 11125 (Blobbonaut Profile) instead of task/task_completed tags
on kind 31124 (Blobbi State).

- Add evolution-missions.ts with static definitions for hatch and evolve
  task pools (TallyMission for interactions, EventMission for themes,
  color moments, posts, profile edits)
- Populate evolution[] in session store on incubation/evolution start;
  clear on stop
- Switch interaction tracking from incrementInteractionTaskTags (kind
  31124 tag manipulation) to trackEvolutionMissionTally (session store)
- Rewrite useHatchTasks/useEvolveTasks to read progress from evolution[]
  and backfill event IDs from retroactive Nostr queries
- Remove useSyncTaskCompletions and the task tag sync effect from
  BlobbiPage

WIP: type errors and barrel exports still need cleanup.
2026-04-16 11:56:16 -05:00
Chad Curtis 3039c46565 Merge branch 'feat/blobbi-retroactive-task-progression' into 'main'
Make hatch/evolve missions count retroactively from user history

Closes #222

See merge request soapbox-pub/ditto!185
2026-04-16 16:18:07 +00:00
Chad Curtis 2d74088b25 Add scroll-to-top feed refresh on Home re-tap and fix mobile tab hover artifact 2026-04-15 20:56:02 -05:00
Alex Gleason 2d52aa8a56 release: v2.7.0 v2.7.0 2026-04-14 16:01:08 -05:00
Alex Gleason 02b83be58e Prevent text selection on long-press of gamepad controls on iOS
Add -webkit-touch-callout: none and -webkit-user-select: none inline
styles to the GameControls container. The existing Tailwind select-none
class (user-select: none) is not sufficient on iOS, where WKWebView
still triggers the long-press callout/highlight gesture on held buttons.
2026-04-14 15:49:16 -05:00
Alex Gleason 8c3371e968 Add native iOS notification polling with rich metadata and grouping
Implement background relay polling for iOS using BGTaskScheduler,
addressing Apple App Store rejection (Guideline 4.2 - Minimum Functionality).

- DittoNotificationPlugin: Capacitor plugin mirroring the Android interface,
  schedules BGAppRefreshTask whenever notifications are enabled (no settings
  change required — both push/persistent modes poll on iOS)
- NostrPoller: fetches notification events via URLSessionWebSocketTask,
  resolves author display names from kind 0 metadata (24h cache), verifies
  referenced event authorship for reactions/reposts/zaps
- Rich notifications with author names, content previews, zap amounts, and
  reaction emoji display
- iOS thread identifiers for native notification grouping per category+post
- Notification categories with summary formats
- Foreground notification display and tap-to-navigate handling
- Immediate poll on app foreground to catch up on missed notifications
- Hide Delivery Method picker on iOS (only meaningful on Android)
2026-04-14 14:58:49 -05:00
Alex Gleason 1a106545f7 Fix haptics: call isNativePlatform() at invocation time, log errors
The platform check was cached as a module-level constant, which could
evaluate before the Capacitor bridge was ready. Moved to per-call checks
matching the pattern used everywhere else in the codebase. Also replaced
silent .catch(() => {}) with console.warn so failures are visible in
Safari Web Inspector / Xcode console.
2026-04-14 14:08:32 -05:00
filemon 86c4594cdd Clean up self-review findings: remove dead exports, simplify query keys, align ceremony state flow
- Remove dead deprecated exports: isValidEvolvePost, EVOLVE_REQUIRED_POSTS,
  BLOBBI_EVOLVE_POST_PREFIX, isValidBlobbiPost, sanitizeToHashtag
- Remove corresponding barrel re-exports from actions/index.ts
- Simplify hatch/evolve query keys to ['...-tasks', pubkey] since
  retroactive queries no longer depend on stateStartedAt
- Drop stateStartedAt from enabled guards so retroactive queries
  aren't blocked when the timestamp is missing
- Align BlobbiHatchingCeremony hatch path: babies now start as
  'evolving' with state_started_at set, matching useBlobbiStageTransition
- Ceremony fakePreview for existing eggs preserves companion's actual state
2026-04-14 14:58:49 -03:00
filemon 6d157c0a65 Merge branch 'main' into feat/blobbi-retroactive-task-progression 2026-04-14 14:13:47 -03:00
filemon 43c75175f4 Auto-start incubation/evolution for new Blobbis
New eggs now start in 'incubating' state with state_started_at set at
adoption time, so hatch tasks begin tracking immediately.

Newly hatched babies now start in 'evolving' state with a fresh
state_started_at, so evolution tasks begin tracking immediately.

The evolving state is applied after validateAndRepairBlobbiTags (which
would otherwise repair task-process states to 'active' via cleanupTaskTags).

Existing/older Blobbis are unaffected -- no migration is performed.
Stop incubation/evolution actions continue to work as before.
2026-04-14 13:47:34 -03:00
Alex Gleason ffa1094f93 Add haptic feedback to Blobbi egg interactions
Hatching ceremony: escalating haptics on each crack click (light → medium
→ heavy → success notification on hatch). Egg tap-to-wiggle in feeds and
posts: light impact on each user-initiated tap. Auto-wiggle intervals are
excluded to avoid unwanted vibration.
2026-04-14 11:44:45 -05:00
Alex Gleason e890e913f5 Fix deep-linking on Google Play version (assetlinks.json update) 2026-04-14 11:42:40 -05:00
Alex Gleason 12a4966b84 Add haptic feedback to emoji reaction selection in QuickReactMenu
The popover emoji picker (both quick presets and full picker) was
publishing reactions internally without triggering haptic feedback.
Add impactLight() at the top of publishReaction() so every emoji
selection path gets tactile feedback.
2026-04-14 11:29:22 -05:00
filemon b68ea276db Make hatch/evolve missions count retroactively from user history
Content-type missions (theme, color moment, post, profile edit) now query
the user's full Nostr history instead of filtering by state_started_at.
Only Blobbi-specific tasks (interactions, maintain_stats) still require
actions on the current Blobbi instance.

Egg incubation:
- create_theme, color_moment: retroactive (no since: filter)
- create_post: retroactive, simplified to any post with #blobbi tag
- interactions: still Blobbi-specific (7x care actions)

Baby evolution:
- create_themes, color_moments, edit_profile: retroactive
- create_posts task removed entirely
- interactions: still Blobbi-specific (21x care actions)
- maintain_stats: still Blobbi-specific (dynamic, all stats >= 80)
2026-04-14 13:27:16 -03:00
Alex Gleason cc702027b0 Add native haptic feedback to all key interactions
Install @capacitor/haptics and add a centralized haptics utility
(src/lib/haptics.ts) that uses the native taptic engine on iOS/Android
and falls back to navigator.vibrate() on web.

Haptics added to:
- Switch component (covers 36+ toggle switches app-wide)
- PullToRefresh threshold (covers 15+ pages)
- MobileBottomNav tab taps
- ReactionButton (like/unlike, double-click heart)
- RepostMenu (repost/undo repost)
- ZapDialog button press + payment success (NWC and WebLN)
- FollowButton and ProfilePage follow toggle
- ComposeBox (post, voice message, and poll publish success)
- NoteMoreMenu (bookmark, pin, mute)
- VinesFeedPage reaction and repost buttons
- ProfileReactionButton and ExternalReactionButton
- NoteCard share button
- BlobbiRoomShell swipe navigation

Replaces raw navigator.vibrate() calls in GameControls and
SendAnimation with the new cross-platform haptics utility, fixing
haptic feedback on iOS where the Vibration API is not available.
2026-04-14 11:06:18 -05:00
Alex Gleason 328c858e4e Merge branch 'fix/emoji-autocomplete-click' into 'main'
Fix emoji/mention autocomplete dropdowns not clickable in compose modal

Closes #221

See merge request soapbox-pub/ditto!184
2026-04-14 02:25:11 +00:00
Mary Kate Fain dcf77aac2a Add pointer-events-auto to autocomplete dropdowns
Radix Dialog's DismissableLayer sets pointer-events: none on
document.body when the modal is open. Since the dropdowns are portaled
to document.body, they inherit this and silently swallow all mouse
events. Adding pointer-events-auto restores click delivery.
2026-04-13 21:14:52 -05:00
Mary Kate Fain cdf3391aad Fix emoji/mention autocomplete dropdowns not clickable in compose modal
The autocomplete dropdowns are portaled to document.body to escape
overflow clipping, which places them outside the Radix Dialog DOM tree.
Clicks on them were treated as 'interact outside' the dialog, preventing
mouse selection of emoji and mention suggestions.

Add data-autocomplete-dropdown attribute to both dropdown containers and
check for it in handleInteractOutside to prevent modal dismissal.
2026-04-13 21:09:58 -05:00
Mary Kate Fain 787446b4ee Update package-lock.json: remove stale dev flags from esbuild optional deps 2026-04-13 20:56:56 -05:00
Mary Kate Fain 5febdb2d7d Increase widget header icon size to match text-xl label 2026-04-13 20:54:08 -05:00
Mary Kate Fain 005f40b536 Increase widget header label to text-xl 2026-04-13 20:52:35 -05:00
Mary Kate Fain 01a6012a0a Simplify sidebar widgets: remove collapse, reposition drag handle, clean up borders 2026-04-13 20:48:36 -05:00