Replace localStorage with an in-memory session store. Kind 11125 content JSON is now the ONLY persistent source of truth for daily missions. Architecture: - On page load / account switch, useDailyMissions hydrates from profile.content.dailyMissions (parsed from the kind 11125 event) - During the session, progress/rerolls update an in-memory Map - Claims persist to kind 11125 via updateDailyMissionsContent() - On page refresh the Map is empty → re-hydrates from kind 11125 - Unclaimed progress is lost on refresh (intentional tradeoff vs cross-account leakage) What changed: - daily-missions.ts: replaced localStorage helpers with in-memory Map<pubkey, DailyMissionsState> (sessionStore). readDailyMissionsState and writeDailyMissionsState now operate on the Map, not localStorage. Added clearDailyMissionsState. Removed getDailyMissionsStorageKey. - useDailyMissions.ts: accepts persistedDailyMissions option (from profile.content.dailyMissions). Hydrates from kind 11125 when the session store is empty. Uses persistedMissionToMission() (previously defined but never called). - useClaimMissionReward.ts: reads from session store instead of localStorage. Still persists to kind 11125 on claim. - useRerollMission.ts: reads/writes session store only. - daily-mission-tracker.ts: reads/writes session store only. Removed ensureCurrentState (no longer creates state — the hook handles init). - BlobbiPage.tsx: passes profile?.content.dailyMissions to useDailyMissions - BlobbiMissionsModal.tsx: passes profile?.content.dailyMissions to useDailyMissions localStorage usage for daily missions: ZERO - No localStorage.getItem calls for blobbi:daily-missions - No localStorage.setItem calls for blobbi:daily-missions - No blobbi:daily-missions key referenced anywhere in the codebase Remaining risks: - Unclaimed progress (feed counts, rerolls) is lost on page refresh since only claims persist to kind 11125. A future enhancement could persist intermediate state on a debounce, but this is out of scope for the foundation phase.
Ditto
Your content. Your vibe. Your rules. A fun, customizable Nostr client that puts you in control.
About
Ditto is an open-source, decentralized social media client built on the Nostr protocol. It's designed for people who want to have fun online without feeding the Big Tech machine. Express yourself with custom themes, Lightning payments, and an ever-growing set of content types -- all while owning your identity and data.
Made by Soapbox.
Features
- Theming -- 9 built-in theme presets, 19 CSS token properties for full customization, and the ability to publish and share themes as Nostr events
- Infinite Content Types -- Text notes, articles, short-form videos (Divines), live streams, polls, follow packs, color moments, magic decks, geocaching, and Webxdc mini-apps
- Lightning Payments -- Zap posts and profiles with sats via Nostr Wallet Connect (NWC) or WebLN
- Private Messaging -- End-to-end encrypted DMs (NIP-04 and NIP-17)
- Comments -- Comment on anything: posts, URLs, profiles, hashtags, books, and more (NIP-22)
- Self-Hosting -- Builds to static HTML/JS/CSS. Deploy anywhere -- GitHub Pages, Netlify, Vercel, a VPS, or a Raspberry Pi
- Mobile -- Android native app via Capacitor, responsive design for all screen sizes
Getting Started
Prerequisites
- Node.js 22+
- npm 10.9.4+
Development
git clone https://gitlab.com/soapbox-pub/ditto.git
cd ditto
npm install
npm run dev
The dev server starts at http://localhost:8080.
Build
npm run build
The built site is output to dist/.
Test
Runs type-checking, linting, unit tests, and a production build:
npm test
Configuration
Ditto is configured through a ditto.json file at the project root, read at build time. This file is gitignored so each deployment can have its own configuration.
{
"theme": "dark",
"relayMetadata": {
"relays": [
{ "url": "wss://relay.ditto.pub", "read": true, "write": true }
]
},
"blossomServers": ["https://blossom.ditto.pub"],
"feedSettings": {
"showPosts": true,
"showReposts": true,
"showArticles": true
// ...and more content type toggles
}
}
Configuration is resolved in three layers (highest priority first):
- User settings stored in localStorage
- Build config from
ditto.json - Hardcoded defaults
Use an alternate config file path with: CONFIG_FILE=./my-config.json npm run build
Custom Branding
For self-hosted instances:
- Replace
public/logo.svgandpublic/logo.pngwith your logo - Update the app name in
index.htmlandpublic/manifest.webmanifest - Replace
public/og-image.jpgfor social sharing previews - Set default relays and upload servers in
ditto.json
Deployment
Ditto builds to static files and can be deployed anywhere that serves HTML.
- GitHub Pages / GitLab Pages -- Push to
mainand CI auto-deploys - Netlify / Vercel -- Connect your fork and deploy. A
_redirectsfile is included for SPA routing - VPS / Any web server -- Build and copy
dist/to your server. Configure SPA routing (e.g., Nginxtry_files $uri $uri/ /index.html)
Android
Build a native Android app with Capacitor:
npm run build
npx cap sync
npx cap open android
Tech Stack
| Layer | Technology |
|---|---|
| Framework | React 18 |
| Build | Vite |
| Language | TypeScript |
| Styling | TailwindCSS 3 + shadcn/ui |
| Routing | React Router 6 |
| Data | TanStack Query |
| Nostr | Nostrify + nostr-tools |
| Mobile | Capacitor |
| Testing | Vitest + React Testing Library |
Project Structure
src/
components/ UI components (100+), including shadcn/ui primitives
hooks/ Custom React hooks (65+)
pages/ Page components for each route (30+)
contexts/ React context providers
lib/ Utilities and shared logic
test/ Test setup and helpers
public/ Static assets, icons, manifest