From 5d26ef6d89df9524c71db5488e3258f71564e9ad Mon Sep 17 00:00:00 2001 From: rosetta Date: Thu, 30 Jul 2026 17:49:58 -0700 Subject: [PATCH] Redesign landing and generate Proto docs --- index.html | 25 +- package-lock.json | 19 +- package.json | 8 +- public/sitemap.xml | 18 +- scripts/generate-proto-docs.mjs | 176 + src/App.tsx | 77 +- src/components/Download/Download.module.css | 12 - src/components/Download/Download.tsx | 50 - .../DownloadCenter/DownloadCenter.module.css | 41 - .../DownloadCenter/DownloadCenter.tsx | 157 - .../FeaturesGrid/FeaturesGrid.module.css | 29 - src/components/FeaturesGrid/FeaturesGrid.tsx | 98 - src/components/Footer/Footer.module.css | 17 - src/components/Footer/Footer.tsx | 28 - src/components/HeroTitle/HeroTitle.module.css | 59 - src/components/HeroTitle/HeroTitle.tsx | 27 - src/components/LegalPage/LegalPage.module.css | 50 +- src/components/LegalPage/LegalPage.tsx | 70 +- .../MessageSteps/MessageSteps.module.css | 28 - src/components/MessageSteps/MessageSteps.tsx | 45 - src/components/Recipient/Recipient.tsx | 45 - src/components/SEO/SEO.tsx | 6 +- src/components/ScrollToTop/ScrollToTop.tsx | 18 +- src/components/Sender/Sender.tsx | 38 - .../SiteFooter/SiteFooter.module.css | 16 + src/components/SiteFooter/SiteFooter.tsx | 66 + .../SiteHeader/SiteHeader.module.css | 58 + src/components/SiteHeader/SiteHeader.tsx | 80 + src/components/Switch/Switch.module.css | 37 - src/components/Switch/Switch.tsx | 51 - src/generated/protoDocs.ts | 3707 +++++++++++++++++ src/pages/LandingPage.module.css | 241 ++ src/pages/LandingPage.tsx | 422 ++ src/pages/PrivacyPage.tsx | 2 +- src/pages/ProtoPage.module.css | 159 + src/pages/ProtoPage.tsx | 483 +++ src/pages/SupportPage.tsx | 1 - src/pages/TermsPage.tsx | 2 +- src/services/updates.ts | 42 + src/style.css | 25 +- src/types/proto.ts | 42 + 41 files changed, 5633 insertions(+), 942 deletions(-) create mode 100644 scripts/generate-proto-docs.mjs delete mode 100644 src/components/Download/Download.module.css delete mode 100644 src/components/Download/Download.tsx delete mode 100644 src/components/DownloadCenter/DownloadCenter.module.css delete mode 100644 src/components/DownloadCenter/DownloadCenter.tsx delete mode 100644 src/components/FeaturesGrid/FeaturesGrid.module.css delete mode 100644 src/components/FeaturesGrid/FeaturesGrid.tsx delete mode 100644 src/components/Footer/Footer.module.css delete mode 100644 src/components/Footer/Footer.tsx delete mode 100644 src/components/HeroTitle/HeroTitle.module.css delete mode 100644 src/components/HeroTitle/HeroTitle.tsx delete mode 100644 src/components/MessageSteps/MessageSteps.module.css delete mode 100644 src/components/MessageSteps/MessageSteps.tsx delete mode 100644 src/components/Recipient/Recipient.tsx delete mode 100644 src/components/Sender/Sender.tsx create mode 100644 src/components/SiteFooter/SiteFooter.module.css create mode 100644 src/components/SiteFooter/SiteFooter.tsx create mode 100644 src/components/SiteHeader/SiteHeader.module.css create mode 100644 src/components/SiteHeader/SiteHeader.tsx delete mode 100644 src/components/Switch/Switch.module.css delete mode 100644 src/components/Switch/Switch.tsx create mode 100644 src/generated/protoDocs.ts create mode 100644 src/pages/LandingPage.module.css create mode 100644 src/pages/LandingPage.tsx create mode 100644 src/pages/ProtoPage.module.css create mode 100644 src/pages/ProtoPage.tsx create mode 100644 src/services/updates.ts create mode 100644 src/types/proto.ts diff --git a/index.html b/index.html index 664bc25..fa78da5 100644 --- a/index.html +++ b/index.html @@ -3,23 +3,23 @@ - Rosetta - Secure & Private Messaging | End-to-End Encrypted Chat - - - + Rosetta — Private Messaging + + + - - + + - - + + - + @@ -28,7 +28,7 @@ "@context": "https://schema.org", "@type": "SoftwareApplication", "name": "Rosetta", - "description": "Secure end-to-end encrypted messaging application", + "description": "Private messaging built around strong encryption, local message history, and user-controlled identity.", "url": "https://rosetta.im", "applicationCategory": "CommunicationApplication", "operatingSystem": ["Windows", "Linux", "Android"], @@ -36,11 +36,6 @@ "@type": "Offer", "price": "0", "priceCurrency": "USD" - }, - "aggregateRating": { - "@type": "AggregateRating", - "ratingValue": "4.8", - "ratingCount": "1500" } } diff --git a/package-lock.json b/package-lock.json index e96e953..8ad14d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,8 @@ "postcss-simple-vars": "^7.0.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "yaml": "^2.9.0" } }, "node_modules/@babel/code-frame": { @@ -3889,6 +3890,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index c0424dd..42d7dfd 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", - "build": "tsc -b && vite build && node build/prepare-sites-build.mjs", + "generate:proto": "node scripts/generate-proto-docs.mjs", + "dev": "npm run generate:proto && vite", + "build": "npm run generate:proto && tsc -b && vite build && node build/prepare-sites-build.mjs", "lint": "eslint .", "preview": "vite preview" }, @@ -39,6 +40,7 @@ "postcss-simple-vars": "^7.0.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "yaml": "^2.9.0" } } diff --git a/public/sitemap.xml b/public/sitemap.xml index 7fef1d6..00bc0d7 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,20 +1,20 @@ - + https://rosetta.im/ - 2026-07-11 + 2026-07-30 weekly 1.0 + + https://rosetta.im/proto/ + 2026-07-30 + weekly + 0.9 + https://rosetta.im/support/ - 2026-07-11 + 2026-07-30 monthly 0.8 diff --git a/scripts/generate-proto-docs.mjs b/scripts/generate-proto-docs.mjs new file mode 100644 index 0000000..71f198c --- /dev/null +++ b/scripts/generate-proto-docs.mjs @@ -0,0 +1,176 @@ +import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; +import { basename, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { isMap, isSeq, parseDocument } from 'yaml'; + +const projectRoot = resolve(fileURLToPath(new URL('..', import.meta.url))); +const sourceDirectory = resolve( + process.env.ROSETTA_PROTO_DIR || resolve(projectRoot, '../rcc/proto'), +); +const outputPath = resolve(projectRoot, 'src/generated/protoDocs.ts'); + +async function exists(path) { + try { + await access(path); + return true; + } catch { + return false; + } +} + +function cleanComment(...comments) { + return comments + .filter(Boolean) + .join('\n') + .split('\n') + .map((line) => line.replace(/^\s*#?\s?/, '').trim()) + .filter(Boolean) + .join(' '); +} + +function mapFields(node) { + if (!isMap(node)) { + return []; + } + + const fields = node.get('fields', true); + if (!isSeq(fields)) { + return []; + } + + return fields.items.map((field) => { + const json = field.toJSON(); + const nameNode = field.get('name', true); + const typeNode = field.get('type', true); + const rawName = String(json.name); + const optional = rawName.endsWith('?'); + + return { + id: Number(json.id), + name: optional ? rawName.slice(0, -1) : rawName, + type: String(json.type), + optional, + description: cleanComment( + field.commentBefore, + field.comment, + nameNode?.commentBefore, + nameNode?.comment, + typeNode?.comment, + ), + }; + }); +} + +function mapNamedCollection(node, kind) { + if (!isMap(node)) { + return []; + } + + return node.items.map((pair) => { + const name = String(pair.key.value); + const description = cleanComment( + pair.key.commentBefore, + pair.key.comment, + pair.value?.commentBefore, + pair.value?.comment, + ); + + if (kind === 'enum') { + const values = pair.value.get('values', true); + return { + name, + type: String(pair.value.get('type')), + description, + values: isSeq(values) + ? values.items.map((value) => { + const json = value.toJSON(); + const nameNode = value.get('name', true); + return { + id: Number(json.id), + name: String(json.name), + description: cleanComment( + value.commentBefore, + value.comment, + nameNode?.commentBefore, + nameNode?.comment, + ), + }; + }) + : [], + }; + } + + const fields = mapFields(pair.value); + return { + name, + id: kind === 'packet' ? Number(pair.value.get('id')) : undefined, + description, + fields, + }; + }); +} + +async function readLayer(path) { + const source = await readFile(path, 'utf8'); + const document = parseDocument(source, { keepSourceTokens: true }); + + if (document.errors.length > 0) { + throw new Error( + `${basename(path)}: ${document.errors.map((error) => error.message).join('; ')}`, + ); + } + + return { + version: Number(document.get('version')), + description: String(document.get('description') || ''), + source: basename(path), + enums: mapNamedCollection(document.get('enums', true), 'enum'), + types: mapNamedCollection(document.get('types', true), 'type'), + packets: mapNamedCollection(document.get('packets', true), 'packet'), + }; +} + +let sourceFiles = []; +try { + sourceFiles = (await readdir(sourceDirectory)) + .filter((file) => /\.ya?ml$/i.test(file)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .map((file) => resolve(sourceDirectory, file)); +} catch { + sourceFiles = []; +} + +if (sourceFiles.length === 0) { + if (await exists(outputPath)) { + console.log( + `Proto sources are unavailable at ${sourceDirectory}; keeping the committed generated documentation.`, + ); + process.exit(0); + } + + throw new Error(`No YAML schema files found in ${sourceDirectory}`); +} + +const layers = []; +for (const file of sourceFiles) { + layers.push(await readLayer(file)); +} +layers.sort((a, b) => a.version - b.version); + +const generatedSource = `/* This file is generated by scripts/generate-proto-docs.mjs. */\nimport type { ProtoLayer } from '../types/proto';\n\nexport const protoLayers = ${JSON.stringify(layers, null, 2)} satisfies ProtoLayer[];\n`; + +await mkdir(resolve(projectRoot, 'src/generated'), { recursive: true }); +await writeFile(outputPath, generatedSource); + +const totals = layers.reduce( + (result, layer) => ({ + enums: result.enums + layer.enums.length, + types: result.types + layer.types.length, + packets: result.packets + layer.packets.length, + }), + { enums: 0, types: 0, packets: 0 }, +); + +console.log( + `Generated Proto docs from ${layers.length} layers: ${totals.packets} packets, ${totals.types} objects, ${totals.enums} enums.`, +); diff --git a/src/App.tsx b/src/App.tsx index f3121cd..53deb6e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,42 +1,57 @@ import '@mantine/core/styles.css'; -import { MantineProvider } from '@mantine/core'; +import { Center, createTheme, Loader, MantineProvider } from '@mantine/core'; +import { lazy, Suspense } from 'react'; import { Route, Routes } from 'react-router-dom'; -import { HeroTitle } from './components/HeroTitle/HeroTitle'; -import { DownloadCenter } from './components/DownloadCenter/DownloadCenter'; -import { FeaturesGrid } from './components/FeaturesGrid/FeaturesGrid'; -import { SEO } from './components/SEO/SEO'; -import './style.css' -import { MessageSteps } from './components/MessageSteps/MessageSteps'; -import { Footer } from './components/Footer/Footer'; -import { PrivacyPage } from './pages/PrivacyPage'; -import { SupportPage } from './pages/SupportPage'; -import { TermsPage } from './pages/TermsPage'; import { ScrollToTop } from './components/ScrollToTop/ScrollToTop'; +import './style.css'; -function LandingPage() { - return ( - <> - - - - - -