## 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/core/ & app/util/ (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" ``` ## File Structure ``` src/ ├── lib/ # Generic reusable code │ ├── components/ # 38 UI components (Button, Dialog, etc.) │ ├── html.ts # DOM utilities │ ├── indexeddb.ts # IndexedDB helpers │ └── util.ts # Generic utilities │ ├── app/ │ ├── core/ │ │ ├── state.ts # State management, stores, constants (687 lines) │ │ ├── commands.ts # Publishing events and other write operations (440+ lines) │ │ ├── requests.ts # Loading data from network (191 lines) │ │ ├── sync.ts # Data synchronization (296 lines) │ │ └── storage.ts # IndexedDB setup │ │ │ ├── util/ │ │ ├── notifications.ts # Push notifications (731 lines) │ │ ├── policies.ts # Relay policies │ │ ├── routes.ts # Routing helpers │ │ ├── modal.ts # Modal management │ │ ├── toast.ts # Toast notifications │ │ ├── theme.ts # Theme switching │ │ └── keyboard.ts # Keyboard handling │ │ │ ├── editor/ # Rich text editor config │ │ ├── index.ts # TipTap setup with Nostr integration │ │ ├── EditorContent.svelte │ │ └── MentionNodeView.ts │ │ │ └── components/ # 188 app-specific components │ ├── Space*.svelte # Space/relay management │ ├── Room*.svelte # Room/channel management │ ├── Chat*.svelte # Direct messaging │ ├── Profile*.svelte # User profiles │ ├── Thread*.svelte # Threaded posts │ └── ... │ ├── routes/ # SvelteKit file-based routing │ ├── +layout.svelte # Root layout (sync logic here) │ ├── spaces/ # Space management │ │ └── [relay]/ # Specific space │ │ ├── chat/ # Space chat │ │ ├── threads/ # Thread posts │ │ ├── calendar/ # Events │ │ └── [h]/ # Specific room (h = room id) │ ├── chat/ # Direct messages │ ├── settings/ # User settings │ └── [bech32]/ # Bech32 entity viewer │ ├── assets/icons/ # ~1,277 SVG icons ├── app.html # HTML template ├── app.css # Global styles └── types.d.ts # Type definitions ``` ## 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 and DaisyUI styling - Only add comments for really weird stuff - Do not call functions in components unless a parameter is reactive. Instead, use a svelte store or rune to make it reactive. - 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 - 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 `ProfileInfo` 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 `