feat: Leave a Whisper + Place a Stone#26
Merged
momentmaker merged 49 commits intomainfrom Apr 7, 2026
Merged
Conversation
Two "leave your mark" features for active walks. Whispers are ephemeral AI-narrated audio messages left at GPS coordinates (1d/7d/1m expiry). Cairns are permanent stone markers that aggregate — multiple pilgrims adding stones to the same location creates a growing cairn with tier-based visuals (faint → eternal at 108 stones). Shared infrastructure: - ProximityDetectionService: 5s throttled location checks against geo-bounded targets (700m whispers, 200m cairns) - GeoCacheService: 50km geo-bounded fetch with ETag/If-None-Match validation, 10km re-fetch threshold, offline sync queue - AudioPriorityQueue: voice guide > whisper > soundscape priority with ducking and deferred playback - HapticManager: centralized haptic patterns with CHHapticEngine for deep stone feedback at higher cairn tiers - CairnTier: 7 visual tiers driving map circle radius/opacity/glow - KanjiExpiryPicker: shared expiry UI with kanji watermarks - ProximityNotificationView: floating map overlay for nearby traces Whisper feature: - 18 pre-made whispers across 6 categories (courage, gratitude, stillness, wonder, compassion, presence) - Category shown as border color, not label text - Max 7 per walk, unlocks at 7 min walk time - Auto-play on proximity (configurable in Sound settings) - WhisperPlayer with preview in placement sheet Cairn feature: - 1 per walk, unlocks at 12 min walk time - 50m merge radius with centroid snapping - SHA-256 generative visual per location - Stone-on-stone sounds with 7 tiers - CairnDetailView shows count + generated art Worker endpoints (pilgrim-worker, separate repo): - GET/POST /api/whispers (geo-bounded, ETag, rate-limited) - GET/POST /api/cairns (atomic increment, centroid, rate-limited) - GET /api/cairns/world (LIMIT 5000, ETag, 1h cache) - Daily cron for whisper expiry cleanup - D1 migration for whispers + cairns tables Still needed before shipping: - 18 whisper narration .m4a audio files - 7 stone-on-stone .m4a audio files - D1 migration applied to production - Worker deployed with new endpoints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Already in .gitignore but was tracked before the rule was added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Re-fetch walk from CoreStore before building submission payload so transcriptions saved after the walk object was loaded are included - Use walk.startDate in user's local timezone instead of UTC Date() for the submission date field Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
108 — the number of mala beads. Minimum stays at 12 minutes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The freshRecordings fetch in PodcastSubmissionService.submit() was crashing because it called DataManager.dataStack.fetchOne from an async Task (background thread). CoreStore requires main-thread access. Wrapped in MainActor.run. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Opens podcast.pilgrimapp.org in an in-app Safari sheet. Order: Podcast → Trail Note → Rate → Share (inward to outward). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7 categories mapped to guides: Presence (Breeze/Eric), Lightness (Drift/Roger), Wonder (Dusk/Bill), Gratitude (Ember/Jessica), Compassion (River/Bella), Courage (Sage/George), Stillness (Stone/Daniel). Generated with ElevenLabs v3 [whispering] tag. Audio on R2 at cdn.pilgrimapp.org/audio/whisper/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- WhisperPlayer: streams audio from cdn.pilgrimapp.org instead of local bundle. Uses AVPlayer for remote URL support. - WhisperPlacementSheet: shows 7 categories (not individual whispers). User picks an energy, app randomly selects a whisper from that category. Preview plays a random whisper. No text shown — the message is a surprise. - Expiry cleanup already deployed in worker cron. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Downloads all 21 whispers to Application Support/Whispers/ on first use via downloadAll(). Plays from local cache when available, falls back to remote URL if not yet downloaded. ~1.5MB total. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Traces section (Leave a Whisper, Place a Stone) only shows when device has network connectivity. Uses NWPathMonitor. Both features require API access, so hiding offline is honest UX. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical: - WhisperPlayer: download task is now cancellable with Task.isCancelled - placeWhisper/placeStone: per-walk limits only consumed on API success, not on failure (prevents silent data loss on spotty connectivity) - ProximityNotificationView: fix deprecated onChange to two-param form - Remove dead offline payload code (makeOfflinePayload) Important: - WalkOptionsSheet: default isConnected=false to prevent flash - StonePlayer: deactivate coordinator before re-activating on new play - WhisperPlayer: remove unused Combine import - WhisperPlayer: preview uses async download for remote URLs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Progressive revelation: pins appear within 2km, grow more vivid as you approach. Whispers pulse at 700m. Cairns scale by tier. - proximityAnnotations() builder: filters cached items to 2km radius, density-limits to 30 pins with 15m minimum separation - Tracks encountered whisper/cairn IDs for future walk summary - Newly placed whispers appear immediately on map via local cache - No tap interaction — walk to things, don't tap them - No animation timers — movement IS the animation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PilgrimMapView: add onAnnotationTap callback with tap gesture recognizer. Hit-tests against whisper/cairn pins within 50m of tap location. - Tap whisper: plays the whisper audio + haptic - Tap cairn: shows CairnDetailView in a bottom sheet (stone visual, count, tier label) - Polish: use raw coordinate math for density filter (no CLLocation allocations), static ISO8601DateFormatter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…w leak Critical: - WhisperPlayer.play(): downloads to cache first if not available, then plays from local file. Fixes AVAudioPlayer crash with HTTP URLs. - WhisperPlayer: downloads all whispers on walk start (not just sheet open) so proximity auto-play works on first encounter. - StonePlayer: graceful fallback if tier-specific audio not found. - Preview task stored and cancelled in stop() to prevent stale playback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tiers 1-3: real stone recordings from Freesound (CC0) Tiers 4-7: generated via ElevenLabs sound effects API - Tier 4: heavy stone thud - Tier 5: resonant rock impact with echo - Tier 6: ceremonial, reverberant - Tier 7: massive, rings like a bell, long decay Note: files need to be added to Xcode project via IDE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Stone placement: haptic fires immediately on tap, sound + state update only on API success - ViewModel stop/cancel: clears all Combine subscriptions and resets proximity service to prevent background GPS/network after walk ends - Reverted GeoCacheService @mainactor (would cascade too many changes; thread safety enforced in practice via Combine receive(on: .main)) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-generated working file that captures session state before conversation compaction. Not persistent — overwritten each time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…feedback - "7 days" → "1 week" in KanjiExpiryPicker - Stone subtitle shows "1 remaining" / "Placed" instead of generic "Available" - Play whisper audio on successful placement as confirmation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- placeStone() now appends a new CachedCairn when creating a brand-new cairn (not just updating existing ones), so the pin appears on the map - Add light haptic feedback on successful waypoint drop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without this, placing a whisper/cairn triggers your own proximity notification ~5 seconds later when the proximity system picks up the new cache entry. Now pre-marks placed items as already-notified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, shareDate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add shareVersion counter to WalkSharingButtons so the view reliably re-evaluates after the share sheet dismisses (onDismiss bumps counter, .id() modifier forces re-render) - Capture Date() once in cacheShare to avoid shareDate/expiry skew Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Whisper, cairn, and waypoint pins don't appear on the active walk map despite correct data flow. Logging at 3 points: proximityAnnotations() output, applyAnnotations() input, and circleManager.annotations set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
References were accidentally removed in 0e84beb when cleaning up duplicate Finder copies — the cleanup deleted all stone-tier refs, not just the duplicates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Xcode kept adding references to Finder's "stone-tier-X 2.m4a" copies instead of the originals. Edited pbxproj directly to reference the correct filenames. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Console.app can read these after disconnected testing. Filter by category "MapDebug" in Console.app with iPhone connected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
42 (the answer), 108 (mala beads / eternal tier), 21 (whisper count). Whisper auto-play requires walking practically on top of it. Cairn notification at about a city block. Merge means the same spot. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "Full Moon" label covers a 3.7-day window where illumination ranges from 100% down to 85%. The shape rendered the shadow faithfully, making a "Full Moon" look gibbous. Now snaps to a perfect circle above 95% and empty path below 5% (new moon). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ring Map pins invisible — annotation managers created before Mapbox style loaded, making their layers dead. Now guard on isStyleLoaded and recreate managers after each style load. Markers placed at walk start — LocationManagement.currentLocationRelay was never updated during active recording, only before walk start. Proximity replay on walk end — stopListening() kills the subscription entirely instead of just clearing notified IDs (race with resetSession). Proximity fires before walk starts — guard on isActiveStatus, reset session on startRecording() so nearby items trigger on first GPS tick. Cache not persisting — locally placed whispers/cairns now persist to UserDefaults. invalidateLastFetch() forces fresh API data each walk. Density filter hiding cairns — same-location whisper+cairn were suppressed as "too close". Now only filters same-type annotations. Icons — whispers rendered as wind SF Symbol in category color, cairns as mountain.2 in moss, waypoints keep their user-chosen symbol. No backing circles. Unique Mapbox image keys per whisper category color. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Mountain icon grows with tier, transitions from outline to filled at medium - Breathing animation (large+), glow ring (great+), golden pulse (eternal) - Kanji watermark per tier: 石 積 道 導 山 聖 永 - Progress bar toward next tier with stone count needed - Relative timestamps for first/last stone placed - Eternal tier shows 108 badge instead of progress bar - Background gradient shifts warm with tier - Merge radius 42m (was 21m) to account for GPS drift - Sheet detent bumped to .medium, removed double padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass version param to bump marketing version for beta builds: gh workflow run testflight.yml --ref branch -f version=1.2.0 Omit version to keep current. Build number always increments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-play whispers is a walk behavior, not an audio setting — moved from Sounds to Practice alongside "Walk with the collective." Trail Note row now uses connectRow with pencil.line icon to match the other Connect card rows. Removed subtext for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Annotation managers now explicitly positioned above pilgrim-route-layer so whisper, cairn, and waypoint icons aren't hidden beneath the path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep feature branch version (1.2.0) and build number. Main only had automated build number increments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Worker changes (pilgrim-worker repo, not in this PR)
GET/POST /api/whispers— geo-bounded fetch + placement with rate limitingGET/POST /api/cairns— atomic stone increment with centroid snapping + rate limitingGET /api/cairns/world— global cairn map (LIMIT 5000, ETag, 1h cache)whispers+cairnstablesStill needed before shipping
.m4aaudio files inSupport Files/Whispers/.m4aaudio files inSupport Files/Stones/Test plan
🤖 Generated with Claude Code