The Featured row already sorted by the moderator's `featured` label
`created_at`, but reordering required clicking Unfeature then Feature
again — clumsy, and the Community grid sorted only by campaign
`created_at` with no moderator input at all.
This commit promotes the existing axis-label `created_at` into a
first-class sort key on both lists and adds drag-and-drop + kebab-row
UI for moderators.
Protocol (no schema change):
- The Featured row sorts by the `featured` label's `created_at`,
newest first (existing behavior).
- The Community grid now sorts by the `approved` label's
`created_at`, newest first (mirroring the Featured row).
- Reordering = republishing the same axis label for the moved
campaign with a chosen `created_at`. Move-to-top stamps `now`;
move-up stamps `neighborAbove.t + 1`; move-down stamps
`neighborBelow.t - 1`. Drag-to-position picks a value between the
two new neighbors.
- No new tags, no new kinds, no new authority — readers that already
understand the moderation namespace pick up the order for free.
- Conflict model unchanged: newest label per (coord, axis) wins.
Implementation:
- `foldModerationLabels` now populates `approvedOrder` alongside
`featuredOrder`.
- `useCampaignModeration().moderate` accepts an optional explicit
`created_at` for the label event (omitted for normal
approve/hide/feature; passed by the reorder hook).
- New `useReorderCampaign` hook with `moveToTop`, `moveUp`,
`moveDown`, and a general `moveTo(toIndex)` used by drag-and-drop.
- New `ReorderableCampaignGrid` wraps a list of `CampaignCard`s:
- non-mods get a plain grid, zero overhead;
- mods on desktop get HTML5 drag-and-drop with a six-dot handle
on hover (the handle is the only `draggable` element so card
clicks still navigate the underlying `<Link>`);
- mods on mobile get Move up / Move down / Move to top rows
injected into the existing moderator kebab via a context
provider (`ReorderProvider` / `useReorderControlsFor`).
- An optimistic local order smooths the gap between publish and
refetch so the card snaps into the new position immediately; it
rolls back automatically on publish failure.
- Translations added in all 15 non-English locales.
- NIP.md documents the ordering convention in a new
"Moderator-driven Ordering" section under the campaign-moderation
surfacing rules.
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.