Collectives P1: participation landing (ES/EN), group tree, room provisioning APIs
- 030: groups.parent_id tree + kind discriminator + group_rooms mapping - /collectives module: create/tree/join APIs, MatrixProvisioner (open room public+federated, closed room E2EE-at-creation, onboarding room), NullProvisioner fail-soft when ARCHIVE_PROVISIONER_* unset - web root becomes the public participation manual (ES-first + EN toggle, three paths: join by invite, start collective, solo account) - participation manual content in core
This commit is contained in:
parent
c31762b82d
commit
4a7e78b28d
23 changed files with 2892 additions and 8 deletions
|
|
@ -14,6 +14,7 @@ export * from "./connection.js";
|
|||
export * from "./file-mime.js";
|
||||
export * from "./file-names.js";
|
||||
export * from "./messaging.js";
|
||||
export * from "./participation-manual.js";
|
||||
export * from "./publishing.js";
|
||||
export * from "./shared-files.js";
|
||||
export * from "./social.js";
|
||||
|
|
|
|||
156
packages/core/src/participation-manual.ts
Normal file
156
packages/core/src/participation-manual.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Participation manual — the bilingual (ES/EN) copy that lives at the app root
|
||||
* and inside every onboarding room (docs/design/2026-10-12-collectives-
|
||||
* architecture.md §"Participation manual").
|
||||
*
|
||||
* Spanish is the primary language: the manual is Spanish-first with an English
|
||||
* toggle, and the provisioner posts the ES sequence before the EN one. The copy
|
||||
* is written for people who organise — plain register, no marketing tone, no
|
||||
* unexplained jargon — and it is deliberately conservative about what the
|
||||
* platform promises (anything security-relevant is stated as behaviour, not as
|
||||
* aspiration).
|
||||
*
|
||||
* This module is the SINGLE source for the copy: the public landing page
|
||||
* renders it and the provisioner bot posts the same text into onboarding rooms,
|
||||
* so the manual a founder reads in the browser is byte-identical to the one the
|
||||
* guide bot posts in Matrix.
|
||||
*/
|
||||
|
||||
/** Manual languages. `es` is the default everywhere (Spanish-first product). */
|
||||
export const MANUAL_LANGUAGES = ["es", "en"] as const;
|
||||
export type ManualLanguage = (typeof MANUAL_LANGUAGES)[number];
|
||||
|
||||
export function isManualLanguage(value: unknown): value is ManualLanguage {
|
||||
return value === "es" || value === "en";
|
||||
}
|
||||
|
||||
/** es first, en second — the order every "post both" path uses. */
|
||||
export const MANUAL_LANGUAGE_ORDER: readonly ManualLanguage[] = ["es", "en"];
|
||||
|
||||
export interface LocalizedManualText {
|
||||
readonly es: string;
|
||||
readonly en: string;
|
||||
}
|
||||
|
||||
/** Paragraphs are separated by a blank line; lines starting with "- " are bullets. */
|
||||
export interface ManualSection {
|
||||
readonly id: string;
|
||||
readonly heading: LocalizedManualText;
|
||||
readonly body: LocalizedManualText;
|
||||
}
|
||||
|
||||
export interface ParticipationPath {
|
||||
readonly id: "join" | "start" | "solo";
|
||||
readonly title: LocalizedManualText;
|
||||
readonly summary: LocalizedManualText;
|
||||
/** What the button/link says on the landing page. */
|
||||
readonly action: LocalizedManualText;
|
||||
}
|
||||
|
||||
export const DEFAULT_MANUAL_LANGUAGE: ManualLanguage = "es";
|
||||
|
||||
export const MANUAL_TITLE: LocalizedManualText = {
|
||||
es: "Manual de participación",
|
||||
en: "Participation manual",
|
||||
};
|
||||
|
||||
export const MANUAL_INTRO: LocalizedManualText = {
|
||||
es: "Este es el punto de entrada público de un colectivo de medios. Antes de entrar, lee esto: explica qué es la plataforma, los tres caminos para participar, cómo funcionan las salas y qué protege el modelo de seguridad. No hace falta cuenta para leer esta página.",
|
||||
en: "This is the public entry point of a media collective. Read this first: it explains what the platform is, the three ways to take part, how the rooms work and what the safety model protects. No account is needed to read this page.",
|
||||
};
|
||||
|
||||
export const PARTICIPATION_MANUAL: readonly ManualSection[] = [
|
||||
{
|
||||
id: "what-this-is",
|
||||
heading: { es: "Qué es esta plataforma", en: "What this platform is" },
|
||||
body: {
|
||||
es: "Un centro de operaciones para colectivos que publican: redactar, revisar, programar y publicar en varias redes a la vez, mantener un archivo que sobrevive a los borradones y coordinar salas en vivo.\n\nCada colectivo tiene su propio árbol de grupos (colectivo y subcolectivos), su carpeta de archivos y su conjunto de salas. Todo funciona en el navegador; no hay nada que instalar.",
|
||||
en: "An operations room for collectives that publish: write, review, schedule and post to several networks at once, keep an archive that survives takedowns, and coordinate live rooms.\n\nEvery collective has its own group tree (the collective and its subcollectives), its own files folder and its own room set. Everything runs in the browser; there is nothing to install.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rooms",
|
||||
heading: { es: "Las salas: abierta, cerrada y de acogida", en: "The rooms: open, closed and onboarding" },
|
||||
body: {
|
||||
es: "- Sala abierta: pública, listada y federada. Cualquier persona desde cualquier servidor Matrix puede entrar a hablar con el colectivo sin pedirnos cuenta. Nunca está cifrada de extremo a extremo: el bot de archivo debe poder leerla para indexar lo que se comparte, así que todo lo que escribas ahí es público.\n- Sala cerrada (producción): solo por invitación y solo en este servidor. Cifrarla de extremo a extremo no es una opción que se activa: la sala nace cifrada y el servidor guarda texto cifrado. La coordinación interna va aquí.\n- Sala de acogida (onboarding): solo por invitación y desechable. El bot guía publica este manual en español y en inglés.\n\nEtiqueta mínima: en la sala abierta habla en nombre propio; la coordinación interna, los datos de personas y las decisiones van en la sala cerrada.",
|
||||
en: "- Open room: public, listed and federated. Anyone from any Matrix server can join and talk to the collective without asking us for an account. It is never end-to-end encrypted: the archive bot has to be able to read it to index what is shared, so everything you write there is public.\n- Closed room (production): invite-only and on this server only. End-to-end encryption is not a switch that gets flipped: the room is born encrypted and the server stores ciphertext. Internal coordination belongs here.\n- Onboarding room: invite-only and disposable. The guide bot posts this manual in Spanish and in English.\n\nMinimum etiquette: in the open room speak in your own name; internal coordination, people's data and decisions belong in the closed room.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "safety",
|
||||
heading: { es: "Modelo de seguridad", en: "Safety model" },
|
||||
body: {
|
||||
es: "- Cifrado de extremo a extremo por defecto en producción: cada sala cerrada se crea ya cifrada; no existe una versión sin cifrar de su contenido en el servidor.\n- Cuentas solo con invitación: el registro está cerrado por token. Quien participa desde fuera lo hace federado, desde su propio servidor, y nunca recibe credenciales nuestras.\n- Borradones: una redacción en Matrix se respeta en el índice y en los enlaces derivados; el material de una sala cerrada vive en los clientes de quienes son miembros. El contenido de una sala abierta era público por definición y se archiva como público.\n- Si el servidor se incauta, lo que se lleva de las salas cerradas es texto cifrado sin claves de lectura.",
|
||||
en: "- End-to-end encryption by default in production: every closed room is created already encrypted; no unencrypted copy of its content exists on the server.\n- Accounts only by invitation: registration is token-gated. Outsiders take part federated, from their own server, and never receive credentials of ours.\n- Redaction: a Matrix redaction is honoured in the index and in any derived links; closed-room material lives in the clients of its members. Open-room content was public by definition and is archived as public.\n- If the server is seized, what leaves the closed rooms is ciphertext without reading keys.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
heading: { es: "Cómo pedir ayuda", en: "How to get help" },
|
||||
body: {
|
||||
es: "Tu sala de acogida es el primer sitio: ahí está este manual en español y en inglés, y el bot guía responde con lo básico.\n\nSi tu enlace de invitación caducó o se agotó, pide uno nuevo a quien lo creó; la caducidad es una medida de seguridad, no un fallo. Si algo urgente pasa en una sala cerrada, escribe en la sala: quien administra el colectivo ve los avisos.",
|
||||
en: "Your onboarding room is the first place to look: this manual is there in Spanish and in English, and the guide bot answers with the basics.\n\nIf your invite link expired or ran out of uses, ask whoever created it for a fresh one; the expiry is a safety measure, not a fault. If something urgent happens in a closed room, write in the room: whoever administers the collective sees the notices.",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** The three participation paths, in the order the landing page shows them. */
|
||||
export const PARTICIPATION_PATHS: readonly ParticipationPath[] = [
|
||||
{
|
||||
id: "join",
|
||||
title: { es: "Únete a un colectivo", en: "Join a collective" },
|
||||
summary: {
|
||||
es: "Te han pasado un enlace de invitación. Ábrelo, crea tu cuenta si aún no tienes y entras directamente al colectivo.",
|
||||
en: "Someone gave you an invite link. Open it, create your account if you do not have one yet, and you land inside the collective.",
|
||||
},
|
||||
action: { es: "Abrir mi invitación", en: "Open my invite" },
|
||||
},
|
||||
{
|
||||
id: "start",
|
||||
title: { es: "Empieza un colectivo", en: "Start a collective" },
|
||||
summary: {
|
||||
es: "Con cuenta activa, creas el colectivo con su nombre y su idioma; las salas (abierta, cerrada y de acogida) se crean solas y quedas como fundador.",
|
||||
en: "With an active account you create the collective with its name and language; the room set (open, closed and onboarding) is provisioned for you and you are its founder.",
|
||||
},
|
||||
action: { es: "Crear un colectivo", en: "Create a collective" },
|
||||
},
|
||||
{
|
||||
id: "solo",
|
||||
title: { es: "Trabaja por tu cuenta", en: "Work on your own" },
|
||||
summary: {
|
||||
es: "Todavía no hay colectivo: entra o crea tu cuenta y empieza en tu propio espacio. Cuando te unas o crees un colectivo, su sala de acogida trae este manual y el bot guía.",
|
||||
en: "No collective yet: sign in or create your account and start in your own workspace. When you join or start a collective, its onboarding room carries this manual and the guide bot.",
|
||||
},
|
||||
action: { es: "Entrar o crear cuenta", en: "Sign in or create account" },
|
||||
},
|
||||
];
|
||||
|
||||
/** Landing-page heading for the three paths block. */
|
||||
export const MANUAL_PATHS_HEADING: LocalizedManualText = {
|
||||
es: "Los tres caminos",
|
||||
en: "The three paths",
|
||||
};
|
||||
|
||||
/** Short header the guide bot posts before each language's manual sequence. */
|
||||
export function onboardingHeader(lang: ManualLanguage): string {
|
||||
return lang === "es"
|
||||
? "Manual de participación — español. La versión en inglés sigue a continuación."
|
||||
: "Participation manual — English. The Spanish version is above.";
|
||||
}
|
||||
|
||||
/**
|
||||
* The manual as the guide bot posts it into an onboarding room: one message per
|
||||
* section, plain text (no HTML), headings inline. The provisioner posts the
|
||||
* primary language's sequence first and the other language second.
|
||||
*/
|
||||
export function onboardingMessages(lang: ManualLanguage): string[] {
|
||||
return [
|
||||
onboardingHeader(lang),
|
||||
...PARTICIPATION_MANUAL.map((section) => `${section.heading[lang]}\n\n${section.body[lang]}`),
|
||||
];
|
||||
}
|
||||
|
||||
/** The order the provisioner posts: the collective's language first, the other second. */
|
||||
export function onboardingLanguageOrder(primary: ManualLanguage): readonly ManualLanguage[] {
|
||||
return primary === "es" ? MANUAL_LANGUAGE_ORDER : ["en", "es"];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue