Three corrections from the previous profile-refocus pass: 1. ProfileOrganizationsStrip is now capped at 4 cards (the design always intended a single hero row on lg+). Overflow surfaces in a 'See all N organizations' modal triggered from the section header so a power-affiliated profile no longer pushes the rest of the page down. The grid switches to 1/2/4 cols at sm/lg to match the 4-card cap. 2. The supplementary rail moves from the left of the tab content to the right. The two-column grid template flips from '[300px_minmax(0,1fr)]' to '[minmax(0,1fr)_320px]', and the JSX reorders <section> before <aside>. The page canvas widens from max-w-6xl to max-w-7xl (matching CampaignsPage / AllCampaignsPage), so on ultrawides the sidebar sits closer to the viewport's right edge instead of floating in the middle of the content area. 3. Kind 16769 (Profile Tabs) is fully removed from ProfilePage. The pencil edit-mode, drag-to-reorder via dnd-kit, '+' Add custom tab menu, ProfileTabEditModal render, ProfileSavedFeedContent renderer, CORE_TAB_FILTERS map, NoTabsEmptyState, and all backing state (tabEditMode, localTabs, tabModalOpen, editingTab, dndSensors, profileTabsQuery, profileTabsData, profileSavedTabs, profileVars, publishProfileTabs) are deleted. The tab list is now the fixed DEFAULT_TAB_LABELS shown identically to owner and visitor, with the four legacy social tabs (Posts & replies, Media, Badges, Likes) reachable through a single overflow dropdown. This drops six imports (useProfileTabs, usePublishProfileTabs, ProfileTabEditModal, useResolveTabFilter, useTabFeed, useActiveTabIndicator), three dnd-kit module imports, the lucide icons Pencil, GripVertical, Plus, and the DropdownMenuSeparator unused after the deletion. The shared kind 16769 infrastructure stays in the codebase because SearchPage and the people-list features still depend on it. The 'hasTabs' gate that protected against the legacy empty-tab-list case is now structurally true and is inlined into the hook calls (useProfileMedia, useProfileLikesInfinite, useWallComments) and tab conditional renderers. Net diff: 154 added, 512 removed in ProfilePage.tsx.
Agora
Power to the people.
Agora is a Nostr client focused on community ownership, expressive identity, and censorship resistance. This repository (agora-3) is the Agora-branded app built from the Ditto codebase.
What This Repo Is
- Agora product identity (name, theme, assets, native IDs)
- Ditto-derived implementation with broad Nostr feature coverage
- Configurable deployment defaults via
agora.json
Features
- Community-first social client: notes, articles, comments, reposts, reactions, and rich event rendering
- Theming system: built-in presets + custom color/font/background themes that can be shared as events
- Lightning support: zaps with Nostr Wallet Connect and WebLN
- Private messaging: NIP-04 and NIP-17 direct messages
- Mobile app shell: Capacitor-powered Android/iOS wrappers
- Self-hostable: static web build + configurable relay and upload infrastructure
Getting Started
Prerequisites
- Node.js 22+
- npm 10.9.4+
Development
git clone https://gitlab.com/soapbox-pub/agora-3.git
cd agora-3
npm install
npm run dev
Development server: http://localhost:8080
Docker Getting Started
Use Docker Compose when you want the nginx reverse-proxy stack (necessary if you want decryptable media in messages - kind 15s of NIP 17):
git clone https://gitlab.com/soapbox-pub/agora-3.git
cd agora-3
cp .env.example .env
docker compose up --build
Proxy URL: http://localhost:8083
This starts:
viteservice on the internal Docker network (vite:8080)webservice (nginx) on host port8082, proxying to Vite with websocket support
Stop stack:
docker compose down
Production-style container build:
docker compose -f docker-compose.prod.yml up --build
Build
npm run build
Build output: dist/
Validate
npm test
This runs type-checking, linting, unit tests, and production build checks.
Configuration
Build-time config is read from agora.json (gitignored by default so each deployment can provide its own values).
{
"theme": "dark",
"relayMetadata": {
"relays": [
{ "url": "wss://relay.ditto.pub", "read": true, "write": true },
{ "url": "wss://relay.primal.net", "read": true, "write": true },
{ "url": "wss://relay.damus.io", "read": true, "write": true }
]
},
"blossomServers": [
"https://blossom.ditto.pub",
"https://blossom.primal.net/"
]
}
Configuration priority (highest first):
- User settings (local storage)
- Build config (
agora.json) - Hardcoded app defaults
Use a custom config path:
CONFIG_FILE=./my-config.json npm run build
Deployment
Agora builds to static files and can be deployed to any static host.
- GitLab/GitHub Pages
- Netlify/Vercel
- VPS or any web server with SPA routing fallback
For Android:
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 |
| Data | TanStack Query |
| Nostr | Nostrify + nostr-tools |
| Mobile | Capacitor |
| Testing | Vitest + React Testing Library |
Contributing
Read CONTRIBUTING.md before opening a merge request.