## Project Overview Flotilla is a Nostr "relays as groups" community chat client. It implements NIP-29 (relay-based groups) to create Discord-like spaces (servers) and rooms (channels). **Tech Stack:** - SvelteKit 5.48+ with TypeScript 5.9+ - Capacitor for cross-platform (Web/PWA, Android, iOS) - TailwindCSS + DaisyUI for styling - Welshman library suite for Nostr protocol - IndexedDB for local storage - Vite for building **Key Concepts:** - **Spaces** - Relays used as community groups (like Discord servers) - **Rooms** - NIP-29 groups within spaces (like Discord channels), identified by `h` - **Chats** - Direct message conversations (NIP-04/NIP-44 encrypted) ## Architecture & Dependency Graph The project follows a **strict acyclic dependency hierarchy**: ``` routes/ (top layer - can depend on anything) ↓ app/components/ (can depend on app/* and lib/*) ↓ app/* (can only depend on lib/*) ↓ lib/ (can only depend on external libraries) ↓ external libraries (bottom layer) ``` **Import Ordering Convention (CRITICAL):** Always sort imports by dependency level: 1. Third-party libraries first 2. Then `lib/` imports 3. Then `app/` imports Example: ```typescript import {derived} from "svelte/store" import {throttle} from "throttle-debounce" import {Dialog} from "$lib/components" import {repository} from "$app/core/state" ``` ## Cleanup Pass These are the things that most often get fixed by hand after the fact. Go through them before handing work back. The full style reference is under Development Conventions below. - **Comments** — only for genuinely surprising things: a workaround, a constraint imposed by a backend or platform, an invariant that isn't visible from the code in front of you. Never comment props, never restate what the next line does, never justify an ordinary decision. If a small refactor would make the comment stale, don't write it. - **Used once, inlined** — a derived value, helper, type, or named constant with a single use is indirection. Write `setTimeout(pollOnce, 3500)`, not a `POLL_INTERVAL` referenced twice in one file. Name something only when the name is what makes the code readable. - **Positive conditionals** — prefer `if (ready) { ... }` over `if (!ready) return`. Nesting is fine; when it gets deep that's the signal the function is doing too much, so split it. Many early returns belong in validation or pipeline functions, not everywhere else. - **Truthiness** — test values directly (`if (invoice.paid_at)`) instead of comparing against `null`/`undefined`, and put the truthy branch first in a ternary. - **Standard names** — `loading` for in-flight state, `on*` for handlers bound to an event, a plain verb (`submit`, `save`) for the action itself. Use `{prop}` shorthand when the names match. - **Errors** — toast a human-readable message and `console.error` anything unexpected (e.g. a non-`HostingError`). Never swallow an error you didn't anticipate. ## State Management **Core Principles:** - Use Svelte 4 **stores** for all state (NOT runes outside UI components) - Most global state flows through Welshman's `repository` (unidirectional) - Query state using `deriveEventsMapped` or `deriveProfile` etc - Update state by publishing events via `publishThunk` **Thunks:** - Reduce UI latency by handling signatures and sending in background - Return status that should be displayed to user - Allow cancellation and error handling - Immediately publish to local repository for optimistic updates ## Nostr Integration **Welshman Library Suite:** - `@welshman/app` - High-level state (pubkey, signer, repository, tracker) - `@welshman/net` - Network layer (Pool, Socket, load, pull, request) - `@welshman/store` - Svelte integration (deriveEventsMapped, etc.) - `@welshman/util` - Event utilities (kinds, tags, validation) - `@welshman/signer` - Signing abstraction (NIP-01, NIP-07, NIP-46) - `@welshman/router` - Relay routing (inbox/outbox model) - `@welshman/editor` - Rich text editor with Nostr - `@welshman/content` - Content parsing - `@welshman/feeds` - Feed management **Key NIPs Implemented:** - NIP-01: Basic protocol - NIP-44/59/17: Encrypted DMs - NIP-07: Browser extension signing - NIP-19: Bech32 encoding - NIP-29: Relay-based Groups - NIP-42: Relay authentication - NIP-43: Relay membership - NIP-46: Nostr Connect (remote signing) - NIP-57: Lightning Zaps ## Development Conventions **Component Parameterization:** - Only pass entity identifiers (`url` for spaces, `h` for rooms) - Derive all other data inside the component from identifiers - Example: Don't pass `members` prop, derive it from `h` inside component **CRITICAL Code Style Guidelines:** - **No `null`** - only use `undefined` - Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components - TailwindCSS styling with css components customized by theme. See lib/components for examples. - Comments, naming, conditionals and single-use indirection are covered by the Cleanup Pass above. - Do not use `any`. If there are type errors related to `unknown`, they are likely because the upstream definition of the data is incorrect. - When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax. - When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss - Do not define svelte event handlers inline, instead name them and put them in the script section of templates - Write a `{#if}`/`{:else if}` chain rather than hoisting display strings into a lookup `Record` in the script section. - Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly. - Instead of `getTag(tagName, event.tags)?.[1] || ""`, use `getTagValue(tagName, event.tags)` - Do not render a profile's `about` directly (e.g. `profile.about`); use the `ProfileAbout` component instead. - Use `type Props` instead of interface when defining props for svelte components. - When a component's value/prop shape mirrors a subset of an existing type, derive it with `Pick`/`Partial` and `export` that type from the component's `