80 lines
6.2 KiB
Markdown
80 lines
6.2 KiB
Markdown
# Server Discovery
|
|
|
|
> **Area:** server-directory
|
|
> **Status:** Active
|
|
> **Last updated:** 2025-02-14
|
|
|
|
## Overview
|
|
|
|
Server discovery lets a signed-in user find public servers to join without knowing an exact name. It spans the signaling **server** (REST routes + CQRS query handlers that rank public servers) and the product **client** (`server-directory` domain API/facade plus the `/dashboard` landing and `/servers` browse page). It complements the existing free-text `GET /api/servers` search with two curated lists — **featured** and **trending**.
|
|
|
|
## Responsibilities
|
|
|
|
- Server: rank and return public servers as **featured** (most-populated) and **trending** (most-recently-active) lists, capped per request.
|
|
- Client: fetch those lists through `ServerDirectoryFacade` and render them via the reusable `app-server-browser` component on `/servers` and `/dashboard`.
|
|
- It does NOT own: free-text search (`GET /api/servers`), join/access checks (`/api/servers/:id/join`), invites, or room signal-affinity. Discovery is read-only browsing; joining flows through existing paths.
|
|
|
|
## Key concepts
|
|
|
|
- **Featured**: public servers ranked by membership count descending, ties broken by most recent `lastSeen` (`rankFeaturedServers`).
|
|
- **Trending**: public servers ranked by most recent `lastSeen` descending, ties broken by membership count (`rankTrendingServers`).
|
|
- **Discovery limit**: each route clamps `limit` to `[1, 50]` (`parseDiscoveryLimit`), default `12`.
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
Both endpoints live in `server/src/routes/servers.ts` and **must be registered before** the parameterised `/:id` route, otherwise Express resolves `featured`/`trending` as a server id.
|
|
|
|
### `GET /api/servers/featured`
|
|
|
|
- **Method**: GET
|
|
- **Authentication**: None (public discovery)
|
|
- **Rate Limiting**: No
|
|
- **Query params**: `limit` (optional integer; clamped to `[1, 50]`, default `12`)
|
|
|
|
### `GET /api/servers/trending`
|
|
|
|
- **Method**: GET
|
|
- **Authentication**: None (public discovery)
|
|
- **Rate Limiting**: No
|
|
- **Query params**: `limit` (optional integer; clamped to `[1, 50]`, default `12`)
|
|
|
|
### Response Schema (both)
|
|
|
|
```json
|
|
{
|
|
"servers": "ServerInfo[] — enriched public servers (icon, channels, sourceId/sourceName/sourceUrl filled by the client API layer)",
|
|
"total": "number — count of servers returned",
|
|
"limit": "number — the effective clamped limit"
|
|
}
|
|
```
|
|
|
|
`ServerInfo` matches the shape returned by `GET /api/servers` search results, so the client normalises and renders all three lists identically.
|
|
|
|
### Error Responses
|
|
|
|
- **500 Internal Server Error**: query handler / persistence failure.
|
|
|
|
---
|
|
|
|
## Server internals
|
|
|
|
- Routes delegate to CQRS query handlers `handleGetFeaturedServers` / `handleGetTrendingServers` (`server/src/cqrs/queries/handlers/`), dispatched via `GetFeaturedServers` / `GetTrendingServers` query types.
|
|
- Ranking lives in `server/src/cqrs/queries/handlers/server-ranking.util.ts` (`rankFeaturedServers`, `rankTrendingServers`, `loadMembershipCounts`). Membership counts load in a single grouped query.
|
|
- Results pass through the same `enrichServer()` step as search before serialisation.
|
|
|
|
## Client internals
|
|
|
|
- `ServerDirectoryApiService.getFeaturedServers()` / `getTrendingServers()` call the routes through a shared private `getDiscoveryServers(path)` helper and normalise into `ServerInfo[]`.
|
|
- `ServerDirectoryService` → `ServerDirectoryFacade` expose `getFeaturedServers()` / `getTrendingServers()` as the domain boundary.
|
|
- `FindServersComponent` (`/servers`) composes **Recently active** (the user's saved rooms, capped at 6), **Featured**, and **Trending** sections, all rendered through `app-server-browser` with `[showMyServers]="true"`.
|
|
- `DashboardComponent` (`/dashboard`) is a single-column landing page (max-width centered, no in-page sidebars): a header greeting (no emoji), a global search with `Ctrl+K` focus and localStorage-backed **Recent Searches** chips shown beneath it, three primary action cards (Find People → `/people`, Find Servers → `/servers`, Create Server → `/create-server` — one link each), and discovery panels **People you might know**, **Popular Servers**, **Your Friends**, and **Recently Active Servers**. Each list is capped at 5 (`DISCOVERY_LIMIT`). It loads `popularServers` on init from `getFeaturedServers(5)`, falling back to `getTrendingServers(5)` when featured is empty; reuses `app-friend-button` for Add and `app-user-avatar` for people rows. `peopleYouMightKnow` excludes existing friends (via `FriendService.friendIds()`); `friends` lists discovered people who are friends. "See all" header links route to the matching `/people` or `/servers` page (no duplicated footer links). Recent searches are recorded on Enter (deduped, most-recent-first, capped at 8) and persisted under `metoyou_dashboard_recent_searches`.
|
|
- The servers-rail top button (`servers-rail.component`) is the **Dashboard** button (`lucideLayoutDashboard`, `title="Dashboard"`); its `goToDashboard()` handler deselects any active voice server and navigates to `/dashboard`. A **Create a server** button (`lucidePlus`, `data-testid="server-rail-create"`) sits below the saved-server icons and opens `app-create-server-dialog` (a Toju modal on desktop / bottom sheet on mobile) which dispatches `RoomsActions.createRoom` directly; the dashboard / `/create-server` route remains as an alternative entry point. Rail icons (`h-12 w-12`, `md:h-11 w-11`) animate their corner radius on hover and `:active` for a Discord-style squircle effect.
|
|
- On mobile (`ViewportService.isMobile()`), `DashboardComponent`, `FindPeopleComponent` (`/people`), and `FindServersComponent` (`/servers`) each mount their page body inside a single `<swiper-container>` slide next to `app-servers-rail` (rail `shrink-0`, content `flex-1` with a left border), mirroring the chat-room / DM-workspace mobile layout so the primary navigation rail stays reachable. The page body is shared between the desktop and mobile branches via an `<ng-template #pageContent>` + `[ngTemplateOutlet]`, and each component declares `schemas: [CUSTOM_ELEMENTS_SCHEMA]` for the Swiper custom elements.
|
|
|
|
## Related
|
|
|
|
- Product-client domain README: `toju-app/src/app/domains/server-directory/README.md`
|
|
- People discovery (`/people`): `toju-app/src/app/domains/direct-message/README.md`
|