Redesign landing and generate Proto docs

This commit is contained in:
rosetta
2026-07-30 17:49:58 -07:00
parent b902298ad6
commit 5d26ef6d89
41 changed files with 5633 additions and 942 deletions

View File

@@ -3,23 +3,23 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rosetta - Secure & Private Messaging | End-to-End Encrypted Chat</title>
<meta name="description" content="Rosetta is a secure, fast, and private messaging app with military-grade end-to-end encryption. Download for Windows, Linux, and Android. No phone number required." />
<meta name="keywords" content="secure messaging, encrypted chat, private messenger, end-to-end encryption, secure communication" />
<meta name="author" content="Rosetta Team" />
<title>Rosetta — Private Messaging</title>
<meta name="description" content="Rosetta is private messaging built around strong encryption, local message history, and user-controlled identity." />
<meta name="keywords" content="Rosetta, encrypted messaging, private messenger, secure chat, protocol documentation" />
<meta name="author" content="Rosetta" />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://rosetta.im/" />
<meta property="og:title" content="Rosetta - Secure Messaging for Everyone" />
<meta property="og:description" content="End-to-end encrypted messaging app with zero-knowledge architecture. Download for Windows, Linux, and Android." />
<meta property="og:title" content="Rosetta — Private Messaging" />
<meta property="og:description" content="Private messaging with strong encryption, local history, and user-controlled identity." />
<meta property="og:site_name" content="Rosetta" />
<meta property="og:locale" content="en_US" />
<meta name="twitter:url" content="https://rosetta.im/" />
<meta name="twitter:title" content="Rosetta - Secure Messaging for Everyone" />
<meta name="twitter:description" content="End-to-end encrypted messaging app with zero-knowledge architecture." />
<meta name="twitter:title" content="Rosetta — Private Messaging" />
<meta name="twitter:description" content="Private messaging with strong encryption, local history, and user-controlled identity." />
<link rel="canonical" href="https://rosetta.im/" />
<link rel="icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" href="/rosetta-logo.png" />
<meta name="theme-color" content="#0066ff" />
<meta name="color-scheme" content="light dark" />
<meta name="format-detection" content="telephone=no" />
@@ -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"
}
}
</script>

19
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -1,20 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:mobile="http://www.mobile.googlebot.com/schemas/mobile/1.0"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:mobile="http://www.mobile.googlebot.com/schemas/mobile/1.0"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://rosetta.im/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-30</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://rosetta.im/proto/</loc>
<lastmod>2026-07-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://rosetta.im/support/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-30</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>

View File

@@ -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.`,
);

View File

@@ -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 (
<>
<SEO />
<HeroTitle />
<FeaturesGrid />
<MessageSteps />
<DownloadCenter />
<Footer />
</>
);
}
const LandingPage = lazy(() =>
import('./pages/LandingPage').then((module) => ({ default: module.LandingPage })),
);
const ProtoPage = lazy(() =>
import('./pages/ProtoPage').then((module) => ({ default: module.ProtoPage })),
);
const SupportPage = lazy(() =>
import('./pages/SupportPage').then((module) => ({ default: module.SupportPage })),
);
const PrivacyPage = lazy(() =>
import('./pages/PrivacyPage').then((module) => ({ default: module.PrivacyPage })),
);
const TermsPage = lazy(() =>
import('./pages/TermsPage').then((module) => ({ default: module.TermsPage })),
);
const theme = createTheme({
primaryColor: 'blue',
defaultRadius: 'md',
fontFamily:
'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
headings: {
fontFamily:
'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
},
});
export default function App() {
return (
<MantineProvider>
<MantineProvider theme={theme}>
<ScrollToTop />
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/support" element={<SupportPage />} />
<Route path="/privacy" element={<PrivacyPage />} />
<Route path="/terms" element={<TermsPage />} />
</Routes>
<Suspense
fallback={
<Center mih="100vh">
<Loader aria-label="Loading page" />
</Center>
}
>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/proto" element={<ProtoPage />} />
<Route path="/support" element={<SupportPage />} />
<Route path="/privacy" element={<PrivacyPage />} />
<Route path="/terms" element={<TermsPage />} />
</Routes>
</Suspense>
</MantineProvider>
)
);
}

View File

@@ -1,12 +0,0 @@
.control {
height: 54px;
padding-left: 38px;
padding-right: 38px;
@media (max-width: $mantine-breakpoint-sm) {
height: 54px;
padding-left: 18px;
padding-right: 18px;
flex: 1;
}
}

View File

@@ -1,50 +0,0 @@
import { Button } from "@mantine/core";
import classes from './Download.module.css';
import { RosettaLogo } from "../RosettaLogo/RosettaLogo";
interface DownloadProps {
href: string;
}
export function Download(props: DownloadProps) {
/*const protocol = useMemo(() => {
let protocol = new Protocol('ws://127.0.0.1:3000');
protocol.addSupportedPacket(new PacketRequestUpdate());
protocol.addSupportedPacket(new PacketKernelUpdate());
return protocol;
}, []);
useEffect(() => {
if(!protocol){
return;
}
const platform = getPlatformTranslated();
let packet = new PacketRequestUpdate();
packet.setPlatform(platform);
packet.setKernelVersion("0.0.0");
packet.setArch("x64");
protocol.sendPacket(packet);
}, [protocol]);
protocol.waitPacket(0x0D, (packet : PacketKernelUpdate) => {
console.info(packet);
});*/
return (
<Button.Group>
<Button
size="xl"
component="a"
className={classes.control}
variant={'default'}
href={props.href}
leftSection={
<RosettaLogo size={25}></RosettaLogo>
}
>
Get Rosetta
</Button>
</Button.Group>
);
}

View File

@@ -1,41 +0,0 @@
.wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: var(--mantine-spacing-xl);
padding-bottom: var(--mantine-spacing-xl);
background-color: #0066ff05;
}
.title {
font-size: 34px;
font-weight: 500;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 24px;
}
}
.description {
max-width: 600px;
margin: auto;
}
.card {
width: 300px;
height: 300px;
border: 1px solid light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
.cardTitle {
&::after {
content: '';
display: block;
background-color: var(--mantine-color-blue-filled);
width: 45px;
height: 2px;
margin-top: var(--mantine-spacing-sm);
}
}

View File

@@ -1,157 +0,0 @@
import {
Button,
Card,
Container,
Flex,
Text,
Title,
useMantineTheme,
} from '@mantine/core';
import classes from './DownloadCenter.module.css';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import { useEffect, useState } from 'react';
import { FaWindows } from 'react-icons/fa';
import { FcAndroidOs, FcLinux } from 'react-icons/fc';
import { Switch } from '../Switch/Switch';
import { IconDownload } from '@tabler/icons-react';
interface DownloadFeature {
platform: string;
arch: string;
link: string;
version: string;
}
interface UpdateItem {
platform: string;
arch: string;
version: string;
downloadUrl: string;
}
const fetchUpdates = async (): Promise<DownloadFeature[]> => {
try {
const response = await fetch('https://sdu.rosetta.im/updates/all');
const data = (await response.json()) as { items?: UpdateItem[] };
return (data.items || []).map((item) => ({
platform: item.platform,
arch: item.arch,
version: item.version,
link: new URL(item.downloadUrl, 'https://sdu.rosetta.im').toString(),
})).sort((a, b) => {
const platformOrder = ['linux', 'win32', 'android'];
const archOrder = ['arm64', 'x64', 'universal']
const platformComparison = platformOrder.indexOf(a.platform) - platformOrder.indexOf(b.platform);
if (platformComparison !== 0) {
return platformComparison;
}
return archOrder.indexOf(a.arch) - archOrder.indexOf(b.arch);
});
} catch (error) {
console.error('Failed to fetch updates:', error);
return [];
}
};
export function DownloadCenter() {
const theme = useMantineTheme();
const [kernels, setKernels] = useState<DownloadFeature[]>([]);
const [targetPlatforms, setTargetPlatforms] = useState<string[]>(['linux', 'win32', 'android']);
useEffect(() => {
fetchUpdates().then(setKernels);
}, []);
const switchTarget = (value: string) => {
let targetPlatforms: string[] = [];
switch(value){
case 'All':
targetPlatforms = ['linux', 'win32', 'android'];
break;
case 'Windows':
targetPlatforms = ['win32'];
break;
case 'Android':
targetPlatforms = ['android'];
break;
case 'Linux':
targetPlatforms = ['linux'];
break;
}
setTargetPlatforms(targetPlatforms);
}
const translatePlatformToOsName = (platform: string) => {
switch(platform){
case 'linux':
return 'Linux';
case 'win32':
return 'Windows';
case 'android':
return 'Android';
default:
return platform;
}
}
const translatePlatformAndArchToCpuName = (platform : string, arch: string) => {
switch(platform){
case 'linux':
return arch === 'arm64' ? 'ARM64' : 'x64';
case 'win32':
return arch === 'arm64' ? 'ARM64' : 'x64';
case 'android':
return 'universal';
default:
return arch;
}
}
const features = kernels.filter((v) => targetPlatforms.includes(v.platform)).map((feature) => (
<Card key={feature.link} radius="md" className={classes.card} padding="xl">
<Flex direction={'column'} align="center" justify={'center'}>
{feature.platform === 'linux' && <FcLinux size={50} />}
{feature.platform === 'win32' && <FaWindows size={50} color={theme.colors.blue[6]} />}
{feature.platform === 'android' && <FcAndroidOs size={50} />}
<Text fz="lg" ta={'center'} fw={500} mt="md">
{translatePlatformToOsName(feature.platform)} - {translatePlatformAndArchToCpuName(feature.platform, feature.arch)}
</Text>
<Text c="dimmed" fz="sm" ta={'center'} mt="xs">
{feature.platform == 'linux' && <>This version is for Linux and comes as an AppImage for the GUI versions.</>}
{feature.platform == 'win32' && <>Download this version of you have computer on Windows 10 or later.</>}
{feature.platform == 'android' && <>This version is for Android devices and all architectures.</>}
</Text>
<Button component={'a'} href={feature.link} variant={'subtle'} mt={'sm'} leftSection={
<IconDownload size={16}></IconDownload>
}>Get {feature.version} now</Button>
</Flex>
</Card>
));
return (
<div className={classes.wrapper}>
<Container id={'kernels'} mb={'xl'} size="lg" mt={'xl'}>
<Flex justify="center" align="center" direction="row" gap={'md'}>
<Title order={2} className={classes.title} ta="center">
Download
</Title>
<RosettaLogo size={35}></RosettaLogo>
</Flex>
<Text c="dimmed" className={classes.description} ta="center" mt="md">
Choose your platform and get started with Rosetta today. Enjoy secure and fast messaging across all your devices.
</Text>
<Flex justify={'center'} mt={'lg'}>
<Switch onChange={switchTarget} data={['All', 'Windows', 'Linux', 'Android']}></Switch>
</Flex>
<Flex gap="xl" justify={'center'} wrap={'wrap'} align={'center'} mt={50}>
{features}
</Flex>
</Container>
</div>
);
}

View File

@@ -1,29 +0,0 @@
.wrapper {
display: flex;
justify-content: flex-start;
flex-direction: column;
background-color: #0066ff05;
min-height: 100vh;
padding-top: var(--mantine-spacing-xl);
padding-bottom: var(--mantine-spacing-xl);
}
.title {
font-family: Outfit, var(--mantine-font-family);
font-weight: 500;
margin-bottom: var(--mantine-spacing-md);
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 28px;
text-align: left;
}
}
.description {
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
text-align: left;
}
}

View File

@@ -1,98 +0,0 @@
import { IconGauge, IconLock, IconMessage2, IconServer, IconUser, IconUsersGroup, type TablerIcon } from '@tabler/icons-react';
import { Container, Flex, SimpleGrid, Text, ThemeIcon, Title } from '@mantine/core';
import type { ReactNode } from 'react';
import classes from './FeaturesGrid.module.css';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
const MOCKDATA = [
{
icon: IconGauge,
title: 'Performance',
description:
'Despite encryption, the messenger remains fast and convenient.',
},
{
icon: IconUser,
title: 'Privacy focused',
description:
'We encrypt everything, even media files, so that no one can access your files.',
},
{
icon: IconServer,
title: 'Server does not save messages',
description:
'The server does not store messages. All messages are stored only on your devices.',
},
{
icon: IconLock,
title: 'Secure by default',
description:
'The messenger uses asymmetric encryption to hide message keys. ChaCha20 is used for message encryption, and AES-256 is used for encrypting your media files.',
},
{
icon: IconMessage2,
title: 'Regular Updates',
description:
'We strive to update the messenger as often as possible to fix bugs and add new features. This is not an abandoned project.',
},
{
icon: IconUsersGroup,
title: 'No trackers or ads',
description:
'Registration without phone number, email, or other personal data. Only public or private keys. We do not use trackers and do not show ads.',
}
];
interface FeatureProps {
icon: TablerIcon;
title: ReactNode;
description: ReactNode;
}
export function Feature({ icon: Icon, title, description }: FeatureProps) {
return (
<div>
<ThemeIcon variant="light" size={40} radius={40}>
<Icon size={18} stroke={1.5} />
</ThemeIcon>
<Text mt="sm" mb={7}>
{title}
</Text>
<Text size="sm" c="dimmed" lh={1.6}>
{description}
</Text>
</div>
);
}
export function FeaturesGrid() {
const features = MOCKDATA.map((feature, index) => <Feature {...feature} key={index} />);
return (
<div className={classes.wrapper}>
<Container>
<Flex justify={'center'} mb={'lg'} align={'center'}>
<RosettaLogo size={50}></RosettaLogo>
</Flex>
<Title ta="center" className={classes.title}>
Here’s what we offer
</Title>
<Container size={560} p={0}>
<Text size="sm" className={classes.description}>
Rosetta offers a wide range of features that ensure security, privacy, and ease of use. Explore the key features that make Rosetta an excellent choice for messaging.
</Text>
</Container>
<SimpleGrid
mt={60}
cols={{ base: 1, sm: 2, md: 3 }}
spacing={{ base: 'xl', md: 50 }}
verticalSpacing={{ base: 'xl', md: 50 }}
>
{features}
</SimpleGrid>
</Container>
</div>
);
}

View File

@@ -1,17 +0,0 @@
.footer {
padding: var(--mantine-spacing-xl) var(--mantine-spacing-md);
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.legal {
max-width: 720px;
margin: 0 auto;
}
.links {
display: flex;
justify-content: center;
gap: var(--mantine-spacing-md);
flex-wrap: wrap;
margin-top: var(--mantine-spacing-xs);
}

View File

@@ -1,28 +0,0 @@
import { Anchor, Text } from '@mantine/core';
import { Link } from 'react-router-dom';
import classes from './Footer.module.css';
const LEGAL_NAME = 'ROSETTA CLOUD SERVICES LIMITED';
export function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className={classes.footer}>
<Text c="dimmed" fz="sm" ta="center" className={classes.legal}>
&copy; {currentYear} {LEGAL_NAME}. All rights reserved.
</Text>
<nav className={classes.links} aria-label="Legal information">
<Anchor component={Link} to="/support" size="sm" c="dimmed">
Support
</Anchor>
<Anchor component={Link} to="/privacy" size="sm" c="dimmed">
Privacy Policy
</Anchor>
<Anchor component={Link} to="/terms" size="sm" c="dimmed">
Terms of Use
</Anchor>
</nav>
</footer>
);
}

View File

@@ -1,59 +0,0 @@
.wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
height: 100vh;
}
.inner {
position: relative;
}
.title {
font-family: Outfit, var(--mantine-font-family);
font-size: 62px;
font-weight: 500;
line-height: 1.1;
margin: 0;
word-wrap: normal;
padding: 0;
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
@media (max-width: $mantine-breakpoint-sm) {
font-size: 42px;
line-height: 1.2;
}
}
.description {
margin-top: var(--mantine-spacing-xl);
font-size: 24px;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 18px;
}
}
.controls {
margin-top: calc(var(--mantine-spacing-xl) * 2);
@media (max-width: $mantine-breakpoint-sm) {
margin-top: var(--mantine-spacing-xl);
}
}
.control {
height: 54px;
padding-left: 38px;
padding-right: 38px;
@media (max-width: $mantine-breakpoint-sm) {
height: 54px;
padding-left: 18px;
padding-right: 18px;
flex: 1;
}
}

View File

@@ -1,27 +0,0 @@
import { Container, Group, Text } from '@mantine/core';
import classes from './HeroTitle.module.css';
import { Download } from '../Download/Download';
export function HeroTitle() {
return (
<div className={classes.wrapper}>
<Container className={classes.inner}>
<h1 className={classes.title}>
Rosetta is{' '}
<Text component="span" variant="gradient" gradient={{ from: 'blue', to: 'cyan' }} inherit>
secure
</Text>{' '}
messaging for everyone
</h1>
<Text className={classes.description} c="dimmed">
Rosetta is designed to ensure secure messaging. We use comprehensive encryption, which keeps the app fast and convenient, yet secure.
</Text>
<Group className={classes.controls}>
<Download href="#kernels"></Download>
</Group>
</Container>
</div>
);
}

View File

@@ -1,34 +1,17 @@
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.shell {
padding-top: var(--mantine-spacing-md);
padding-bottom: calc(var(--mantine-spacing-xl) * 2);
}
.header {
min-height: 58px;
padding-bottom: var(--mantine-spacing-md);
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
.brand {
display: inline-flex;
align-items: center;
gap: var(--mantine-spacing-xs);
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
text-decoration: none;
}
.brand img {
border-radius: var(--mantine-radius-sm);
flex: 1;
}
.main {
width: 100%;
padding-top: 96px;
padding: 96px 0 112px;
}
.intro {
@@ -46,10 +29,6 @@
line-height: 1.08;
}
.document {
padding-bottom: 72px;
}
.lead {
max-width: 780px;
margin: 0;
@@ -124,14 +103,9 @@
white-space: pre-line;
}
.footer {
padding-top: var(--mantine-spacing-lg);
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
@media (max-width: 48em) {
.main {
padding-top: 72px;
padding: 72px 0 88px;
}
.intro {
@@ -147,17 +121,7 @@
}
@media (max-width: 36em) {
.shell {
padding-inline: var(--mantine-spacing-md);
}
.header {
align-items: flex-start;
flex-direction: column;
}
.footer {
align-items: flex-start;
flex-direction: column;
.main {
padding-top: 56px;
}
}

View File

@@ -1,22 +1,17 @@
import type { ReactNode } from 'react';
import {
Anchor,
Box,
Container,
Group,
List,
Paper,
Text,
Title,
} from '@mantine/core';
import { Link } from 'react-router-dom';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import { SiteFooter } from '../SiteFooter/SiteFooter';
import { SiteHeader } from '../SiteHeader/SiteHeader';
import classes from './LegalPage.module.css';
type ContentRoute = 'support' | 'privacy' | 'terms';
interface LegalPageProps {
active: ContentRoute;
title: string;
updated?: string;
eyebrow?: string;
@@ -28,54 +23,16 @@ interface LegalSectionProps {
children: ReactNode;
}
const NAV_LINKS = [
{ route: 'support', label: 'Support' },
{ route: 'privacy', label: 'Privacy' },
{ route: 'terms', label: 'Terms' },
] as const;
export function LegalPage({
active,
title,
updated,
eyebrow = 'Rosetta Legal',
children,
}: LegalPageProps) {
const currentYear = new Date().getFullYear();
return (
<Box className={classes.page}>
<SiteHeader />
<Container className={classes.shell}>
<Group component="header" justify="space-between" className={classes.header}>
<Anchor
component={Link}
to="/"
className={classes.brand}
aria-label="Rosetta"
>
<RosettaLogo size={34} />
<Text component="span" fw={700} fz="lg">
Rosetta
</Text>
</Anchor>
<Group component="nav" gap="lg" aria-label="Rosetta pages">
{NAV_LINKS.map(({ route, label }) => (
<Anchor
key={route}
component={Link}
to={`/${route}`}
c={active === route ? 'blue' : 'dimmed'}
fw={active === route ? 600 : 400}
size="sm"
aria-current={active === route ? 'page' : undefined}
>
{label}
</Anchor>
))}
</Group>
</Group>
<Box component="main" className={classes.main}>
<Box className={classes.intro}>
<Text tt="uppercase" fw={700} fz="xs" c="blue" lts="0.08em">
@@ -95,27 +52,8 @@ export function LegalPage({
{children}
</Box>
</Box>
<Group component="footer" justify="space-between" className={classes.footer}>
<Text c="dimmed" fz="xs">
&copy; {currentYear} ROSETTA CLOUD SERVICES LIMITED
</Text>
<Group component="nav" gap="md" aria-label="Footer links">
<Anchor component={Link} to="/" c="dimmed" size="xs">
Rosetta
</Anchor>
<Anchor component={Link} to="/support" c="dimmed" size="xs">
Support
</Anchor>
<Anchor component={Link} to="/privacy" c="dimmed" size="xs">
Privacy Policy
</Anchor>
<Anchor component={Link} to="/terms" c="dimmed" size="xs">
Terms of Use
</Anchor>
</Group>
</Group>
</Container>
<SiteFooter />
</Box>
);
}

View File

@@ -1,28 +0,0 @@
.wrapper {
display: flex;
justify-content: flex-start;
flex-direction: column;
min-height: 100vh;
padding-top: var(--mantine-spacing-xl);
padding-bottom: var(--mantine-spacing-xl);
}
.title {
font-family: Outfit, var(--mantine-font-family);
font-weight: 500;
margin-bottom: var(--mantine-spacing-md);
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 28px;
text-align: left;
}
}
.description {
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
text-align: left;
}
}

View File

@@ -1,45 +0,0 @@
import { Container, Flex, Text, Title } from '@mantine/core';
import classes from './MessageSteps.module.css';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import { Switch } from '../Switch/Switch';
import { useState } from 'react';
import { Sender } from '../Sender/Sender';
import { Recipient } from '../Recipient/Recipient';
export function MessageSteps() {
const [show, setShow] = useState('Sender');
return (
<div className={classes.wrapper}>
<Container>
<Flex justify={'center'} mb={'lg'} align={'center'}>
<RosettaLogo size={50}></RosettaLogo>
</Flex>
<Title ta="center" className={classes.title}>
How is my message sent?
</Title>
<Container size={560} p={0}>
<Text size="sm" className={classes.description}>
See why Rosetta is truly secure. Notice how your message is sent and received from you to the recipient. The steps highlighted in blue are what you see, while the steps not highlighted are what happens behind the scenes.
</Text>
</Container>
<Flex align={'center'} justify={'center'} mt={'xl'}>
<Switch data={[
'Sender',
'Recipient'
]} onChange={(v) => setShow(v)}></Switch>
</Flex>
<Flex justify={'center'} align={'center'} mt={'xl'} gap={'xl'}>
<Flex>
{show == 'Sender' && <Sender></Sender>}
{show == 'Recipient' && <Recipient></Recipient>}
</Flex>
</Flex>
</Container>
</div>
);
}

View File

@@ -1,45 +0,0 @@
import { Text, Timeline } from "@mantine/core";
import { IconDeviceDesktop, IconKey, IconLockAccess, IconMessage, IconMessage2, IconServer } from "@tabler/icons-react";
export function Recipient() {
return (
<Timeline reverseActive active={0} bulletSize={40} lineWidth={2}>
<Timeline.Item bullet={<IconServer size={20} />} title="Retranslate">
<Text c="dimmed" size="sm">
Server retranslate message to recipient.
</Text>
<Text size="xs" mt={4}>After retranslate server deletes message from memory.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconMessage size={20} />} title="Receive message">
<Text c="dimmed" size="sm">
Recipient device receives encrypted message and encrypted ChaCha20 key from server.
</Text>
<Text size="xs" mt={4}>Message now encrypted.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconLockAccess size={20} />} title="Decrypting ChaCha20 key">
<Text c="dimmed" size="sm">
Decrypt ChaCha20 key with recipient private key using RSA-4096 decryption.
</Text>
<Text size="xs" mt={4}>Message still encrypted.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconKey size={20} />} title="Decrypting message using ChaCha20">
<Text c="dimmed" size="sm">
Decrypting text of message or attachments using decrypted ChaCha20 key from previous step.
</Text>
<Text size="xs" mt={4}>Message text is available.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconDeviceDesktop size={20} />} title="Save message on your device">
<Text c="dimmed" size="sm">
Encrypt decrypted text with AES-256 for save in your device.
</Text>
<Text size="xs" mt={4}>Need for future access, because message not save on server.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconMessage2 size={20} />} title="Show message">
<Text c="dimmed" size="sm">
Message available for reading in your chat.
</Text>
<Text size="xs" mt={4}>Final step.</Text>
</Timeline.Item>
</Timeline>
)
}

View File

@@ -8,9 +8,9 @@ interface SEOProps {
}
export function SEO({
title = 'Rosetta - Secure Messaging for Everyone',
description = 'End-to-end encrypted messaging app with military-grade security. Download for Windows, Linux, and Android.',
keywords = 'secure messaging, encrypted chat, private messenger',
title = 'Rosetta — Private Messaging',
description = 'Private messaging built around strong encryption, local message history, and user-controlled identity.',
keywords = 'Rosetta, encrypted messaging, private messenger, secure chat',
canonical = 'https://rosetta.im/'
}: SEOProps) {
useEffect(() => {

View File

@@ -2,11 +2,23 @@ import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function ScrollToTop() {
const { pathname } = useLocation();
const { pathname, hash } = useLocation();
useEffect(() => {
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
}, [pathname]);
const frame = window.requestAnimationFrame(() => {
if (hash) {
const target = document.getElementById(decodeURIComponent(hash.slice(1)));
if (target) {
target.scrollIntoView();
return;
}
}
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
});
return () => window.cancelAnimationFrame(frame);
}, [pathname, hash]);
return null;
}

View File

@@ -1,38 +0,0 @@
import { Text, Timeline } from "@mantine/core";
import { IconDeviceDesktop, IconKering, IconKey, IconMessage, IconServer } from "@tabler/icons-react";
export function Sender() {
return (
<Timeline active={0} bulletSize={40} lineWidth={2}>
<Timeline.Item bullet={<IconMessage size={20} />} title="Enter message">
<Text c="dimmed" size="sm">
You enter message text in chat.
</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconDeviceDesktop size={20} />} title="Encrypt for you">
<Text c="dimmed" size="sm">
Message encrypted for your device using AES-256 encryption.
</Text>
<Text size="xs" mt={4}>Need for safety save message on your device.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconKey size={20} />} title="Encrypt for opponent">
<Text c="dimmed" size="sm">
Encrypt message for recipient using ChaCha20 encryption.
</Text>
<Text size="xs" mt={4}>Encrypt message with chacha20 with key.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconKering size={20} />} title="Encrypt ChaCha20 key to send">
<Text c="dimmed" size="sm">
Encrypt key of previous step with recipient public key using RSA-4096 encryption.
</Text>
<Text size="xs" mt={4}>Needs for send encrypted key to opponent.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconServer size={20} />} title="Send encrypted data to server">
<Text c="dimmed" size="sm">
Send encrypted message and encrypted ChaCha20 key to server for delivery to recipient.
</Text>
<Text size="xs" mt={4}>See next steps in recipient timeline.</Text>
</Timeline.Item>
</Timeline>
)
}

View File

@@ -0,0 +1,16 @@
.footer {
padding: 72px 0 32px;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
}
.footer a {
width: fit-content;
text-decoration: none;
}
.copyright {
margin-top: 56px;
padding-top: var(--mantine-spacing-lg);
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
}

View File

@@ -0,0 +1,66 @@
import { Anchor, Container, Group, SimpleGrid, Stack, Text } from '@mantine/core';
import { Link } from 'react-router-dom';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import classes from './SiteFooter.module.css';
export function SiteFooter() {
const year = new Date().getFullYear();
return (
<footer className={classes.footer}>
<Container>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl">
<Stack gap="sm">
<Group gap="sm">
<RosettaLogo size={34} />
<Text fw={700} fz="lg">
Rosetta
</Text>
</Group>
<Text c="dimmed" size="sm" maw={320} lh={1.6}>
Private messaging built around strong encryption, local history, and
user-controlled identity.
</Text>
</Stack>
<Stack gap="xs">
<Text fw={600} size="sm">
Product
</Text>
<Anchor component={Link} to="/#features" c="dimmed" size="sm">
Features
</Anchor>
<Anchor component={Link} to="/#apps" c="dimmed" size="sm">
Apps
</Anchor>
<Anchor component={Link} to="/proto" c="dimmed" size="sm">
Proto
</Anchor>
<Anchor component={Link} to="/support" c="dimmed" size="sm">
Support
</Anchor>
</Stack>
<Stack gap="xs">
<Text fw={600} size="sm">
Legal
</Text>
<Anchor component={Link} to="/privacy" c="dimmed" size="sm">
Privacy Policy
</Anchor>
<Anchor component={Link} to="/terms" c="dimmed" size="sm">
Terms of Use
</Anchor>
<Anchor href="mailto:admin@rosetta.im" c="dimmed" size="sm">
Admin@rosetta.im
</Anchor>
</Stack>
</SimpleGrid>
<Text c="dimmed" size="xs" className={classes.copyright}>
&copy; {year} ROSETTA CLOUD SERVICES LIMITED
</Text>
</Container>
</footer>
);
}

View File

@@ -0,0 +1,58 @@
.header {
position: sticky;
z-index: 50;
top: 0;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
background: light-dark(rgba(255, 255, 255, 0.92), rgba(26, 27, 30, 0.92));
backdrop-filter: blur(16px);
}
.inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 72px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
text-decoration: none;
}
.brand img {
border-radius: 9px;
}
.link {
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: var(--mantine-font-size-sm);
font-weight: 500;
text-decoration: none;
transition: color 120ms ease;
}
.link:hover,
.link[data-active] {
color: var(--mantine-color-blue-6);
}
.burger {
display: none;
}
.drawerTitle {
font-weight: 700;
}
@media (max-width: 48em) {
.navigation {
display: none;
}
.burger {
display: block;
}
}

View File

@@ -0,0 +1,80 @@
import {
Anchor,
Burger,
Container,
Drawer,
Group,
Stack,
Text,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Link, useLocation } from 'react-router-dom';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import classes from './SiteHeader.module.css';
const NAVIGATION = [
{ label: 'Features', to: '/#features' },
{ label: 'Apps', to: '/#apps' },
{ label: 'Proto', to: '/proto' },
{ label: 'Support', to: '/support' },
];
export function SiteHeader() {
const [opened, { close, toggle }] = useDisclosure(false);
const { pathname } = useLocation();
const links = NAVIGATION.map(({ label, to }) => {
const active =
(to === '/proto' && pathname === '/proto') ||
(to === '/support' && pathname === '/support');
return (
<Anchor
key={to}
component={Link}
to={to}
onClick={close}
className={classes.link}
data-active={active || undefined}
>
{label}
</Anchor>
);
});
return (
<header className={classes.header}>
<Container className={classes.inner}>
<Anchor component={Link} to="/" className={classes.brand} aria-label="Rosetta">
<RosettaLogo size={36} />
<Text component="span" fw={700} fz="lg">
Rosetta
</Text>
</Anchor>
<Group component="nav" gap="xl" className={classes.navigation}>
{links}
</Group>
<Burger
opened={opened}
onClick={toggle}
size="sm"
className={classes.burger}
aria-label="Toggle navigation"
/>
</Container>
<Drawer
opened={opened}
onClose={close}
position="right"
size="xs"
title="Rosetta"
classNames={{ title: classes.drawerTitle }}
>
<Stack gap="lg">{links}</Stack>
</Drawer>
</header>
);
}

View File

@@ -1,37 +0,0 @@
.root {
position: relative;
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
width: fit-content;
border-radius: var(--mantine-radius-md);
padding: 5px;
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
}
.control {
padding: 7px 12px;
line-height: 1;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2));
border-radius: var(--mantine-radius-md);
font-size: var(--mantine-font-size-sm);
transition: color 100ms ease;
font-weight: 500;
@mixin hover {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
}
&[data-active] {
color: var(--mantine-color-white);
}
}
.controlLabel {
position: relative;
z-index: 1;
}
.indicator {
background-color: var(--mantine-primary-color-filled);
border-radius: var(--mantine-radius-md);
}

View File

@@ -1,51 +0,0 @@
import { useState } from 'react';
import { FloatingIndicator, UnstyledButton } from '@mantine/core';
import classes from './Switch.module.css';
interface SwitchProps {
data: string[];
onChange?: (value: string) => void;
}
export function Switch(props: SwitchProps) {
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
const [active, setActive] = useState(0);
const setControlRef = (index: number) => (node: HTMLButtonElement) => {
controlsRefs[index] = node;
setControlsRefs(controlsRefs);
};
const changeActive = (index: number) => {
setActive(index);
if(!props.onChange){
return;
}
props.onChange(props.data[index]);
}
const controls = props.data.map((item, index) => (
<UnstyledButton
key={item}
className={classes.control}
ref={setControlRef(index)}
onClick={() => changeActive(index)}
mod={{ active: active === index }}
>
<span className={classes.controlLabel}>{item}</span>
</UnstyledButton>
));
return (
<div className={classes.root} ref={setRootRef}>
{controls}
<FloatingIndicator
target={controlsRefs[active]}
parent={rootRef}
className={classes.indicator}
/>
</div>
);
}

3707
src/generated/protoDocs.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,241 @@
.hero {
overflow: hidden;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.heroGrid {
display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(340px, 0.92fr);
gap: 72px;
min-height: calc(100vh - 72px);
padding-top: 72px;
padding-bottom: 72px;
}
.heroTitle {
max-width: 720px;
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: clamp(52px, 7vw, 76px);
font-weight: 600;
letter-spacing: -0.045em;
line-height: 1.02;
}
.heroDescription {
max-width: 680px;
margin-top: 28px;
font-size: 20px;
line-height: 1.65;
}
.preview {
position: relative;
display: grid;
place-items: center;
min-height: 640px;
}
.preview::before {
position: absolute;
width: 520px;
height: 520px;
border-radius: 50%;
background: light-dark(rgba(34, 139, 230, 0.11), rgba(34, 139, 230, 0.16));
content: '';
filter: blur(2px);
}
.phone {
position: relative;
z-index: 1;
width: min(100%, 380px);
padding: 20px;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
}
.phoneHeader {
padding-bottom: var(--mantine-spacing-md);
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.messages {
padding: 32px 0 24px;
}
.messageIncoming,
.messageOutgoing {
max-width: 84%;
}
.messageIncoming {
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
}
.messageOutgoing {
margin-left: auto;
background: light-dark(var(--mantine-color-blue-0), rgba(34, 139, 230, 0.18));
}
.encryptionNote {
color: var(--mantine-color-blue-6);
padding-top: var(--mantine-spacing-md);
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.floatingCard {
position: absolute;
z-index: 2;
right: -24px;
bottom: 84px;
display: flex;
align-items: center;
gap: var(--mantine-spacing-sm);
padding: var(--mantine-spacing-md);
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
}
.featuresSection,
.appsSection,
.protocolSection {
padding: 112px 0;
}
.featuresSection {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
}
.appsSection {
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.sectionHeading {
max-width: 720px;
}
.sectionHeading h2 {
margin-top: var(--mantine-spacing-xs);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: clamp(36px, 5vw, 52px);
font-weight: 600;
letter-spacing: -0.035em;
line-height: 1.08;
}
.sectionHeading > p:last-child {
margin-top: var(--mantine-spacing-md);
font-size: 18px;
line-height: 1.65;
}
.featureTitle,
.appTitle {
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-size: 19px;
font-weight: 600;
}
.appCard {
min-height: 320px;
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
}
.protocolSection {
padding-top: 0;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.protocolCard {
display: flex;
align-items: center;
justify-content: space-between;
gap: 48px;
padding: 56px;
background: linear-gradient(135deg, var(--mantine-color-blue-8), var(--mantine-color-blue-5));
}
.protocolCard h2 {
font-size: clamp(32px, 4vw, 46px);
font-weight: 600;
letter-spacing: -0.03em;
line-height: 1.1;
}
.protocolButton {
flex: 0 0 auto;
}
@media (max-width: 62em) {
.heroGrid {
grid-template-columns: 1fr;
gap: 28px;
padding-top: 88px;
}
.preview {
min-height: 560px;
}
.floatingCard {
right: 8%;
}
}
@media (max-width: 48em) {
.heroGrid {
min-height: auto;
padding-top: 64px;
padding-bottom: 64px;
}
.heroTitle {
font-size: clamp(44px, 13vw, 62px);
}
.heroDescription {
font-size: 18px;
}
.preview {
min-height: 520px;
}
.floatingCard {
right: 0;
bottom: 40px;
}
.featuresSection,
.appsSection,
.protocolSection {
padding: 80px 0;
}
.protocolSection {
padding-top: 0;
}
.protocolCard {
align-items: flex-start;
flex-direction: column;
padding: 36px 28px;
}
}
@media (max-width: 36em) {
.heroCopy :global(.mantine-Group-root) {
align-items: stretch;
}
.heroCopy :global(.mantine-Button-root) {
width: 100%;
}
.preview {
min-height: 480px;
}
.floatingCard {
display: none;
}
}

422
src/pages/LandingPage.tsx Normal file
View File

@@ -0,0 +1,422 @@
import {
Badge,
Box,
Button,
Card,
Container,
Group,
Paper,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
} from '@mantine/core';
import {
IconArrowDown,
IconBolt,
IconBrandAndroid,
IconBrandWindows,
IconCheck,
IconCode,
IconDatabaseOff,
IconDeviceDesktop,
IconFileTextShield,
IconKey,
IconLock,
IconMessageCircle,
IconPhoneOff,
type TablerIcon,
} from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { RosettaLogo } from '../components/RosettaLogo/RosettaLogo';
import { SEO } from '../components/SEO/SEO';
import { SiteFooter } from '../components/SiteFooter/SiteFooter';
import { SiteHeader } from '../components/SiteHeader/SiteHeader';
import { fetchUpdates, type DownloadRelease } from '../services/updates';
import classes from './LandingPage.module.css';
const FEATURES: Array<{ icon: TablerIcon; title: string; description: string }> = [
{
icon: IconLock,
title: 'Private by design',
description:
'Messages and media are encrypted before they leave your device, with keys kept under your control.',
},
{
icon: IconPhoneOff,
title: 'No phone number',
description:
'Create an identity without sharing a phone number, email address, or advertising profile.',
},
{
icon: IconDatabaseOff,
title: 'Local message history',
description:
'Readable message history lives on your devices instead of becoming a permanent server archive.',
},
{
icon: IconFileTextShield,
title: 'Encrypted media',
description:
'Photos, videos, voice messages, and files use the same privacy-first transport model.',
},
{
icon: IconBolt,
title: 'Fast across platforms',
description:
'A focused protocol and native clients keep everyday conversations responsive on desktop and mobile.',
},
{
icon: IconCode,
title: 'Documented protocol',
description:
'Every packet, object, enum, and field is published from the protocol source schemas.',
},
];
const PLATFORMS: Array<{
key: string;
title: string;
description: string;
icon: TablerIcon;
}> = [
{
key: 'android',
title: 'Rosetta for Android',
description: 'Private messaging for Android phones and tablets.',
icon: IconBrandAndroid,
},
{
key: 'win32',
title: 'Rosetta for Windows',
description: 'A native desktop client for Windows 10 and later.',
icon: IconBrandWindows,
},
{
key: 'linux',
title: 'Rosetta for Linux',
description: 'Desktop builds for x64 and ARM64 Linux systems.',
icon: IconDeviceDesktop,
},
];
function architectureName(arch: string) {
if (arch === 'arm64') {
return 'ARM64';
}
if (arch === 'universal') {
return 'Universal';
}
return arch;
}
function DownloadApps() {
const [releases, setReleases] = useState<DownloadRelease[]>([]);
const [loading, setLoading] = useState(true);
const [failed, setFailed] = useState(false);
useEffect(() => {
const controller = new AbortController();
let active = true;
fetchUpdates(controller.signal)
.then((items) => {
if (active) {
setReleases(items);
}
})
.catch((error: unknown) => {
if (active && error instanceof Error && error.name !== 'AbortError') {
setFailed(true);
}
})
.finally(() => {
if (active) {
setLoading(false);
}
});
return () => {
active = false;
controller.abort();
};
}, []);
const groupedReleases = useMemo(
() =>
releases.reduce<Record<string, DownloadRelease[]>>((groups, release) => {
groups[release.platform] ||= [];
groups[release.platform].push(release);
return groups;
}, {}),
[releases],
);
return (
<Box component="section" id="apps" className={classes.appsSection}>
<Container>
<Box className={classes.sectionHeading}>
<Text c="blue" fw={700} size="sm">
Rosetta Apps
</Text>
<Title order={2}>One conversation, every device.</Title>
<Text c="dimmed">
Select your platform. Available versions are loaded directly from the
Rosetta update service.
</Text>
</Box>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="lg" mt={48}>
{PLATFORMS.map(({ key, title, description, icon: Icon }) => {
const platformReleases = groupedReleases[key] || [];
return (
<Card key={key} withBorder radius="lg" padding="xl" className={classes.appCard}>
<ThemeIcon size={52} radius="xl" variant="light">
<Icon size={27} stroke={1.6} />
</ThemeIcon>
<Title order={3} mt="lg" className={classes.appTitle}>
{title}
</Title>
<Text c="dimmed" size="sm" mt="xs" lh={1.6}>
{description}
</Text>
<Stack gap="xs" mt="xl">
{platformReleases.map((release) => (
<Button
key={release.link}
component="a"
href={release.link}
variant="light"
justify="space-between"
rightSection={<IconArrowDown size={16} />}
>
{architectureName(release.arch)} · {release.version}
</Button>
))}
{loading && (
<Text c="dimmed" size="sm">
Checking the latest release…
</Text>
)}
{!loading && platformReleases.length === 0 && (
<Text c="dimmed" size="sm">
{failed
? 'Release service is temporarily unavailable.'
: 'No active release for this platform.'}
</Text>
)}
</Stack>
</Card>
);
})}
</SimpleGrid>
</Container>
</Box>
);
}
export function LandingPage() {
return (
<>
<SEO
title="Rosetta — Private Messaging"
description="Rosetta is private messaging built around strong encryption, local message history, and user-controlled identity."
canonical="https://rosetta.im/"
keywords="Rosetta, encrypted messaging, private messenger, secure chat"
/>
<SiteHeader />
<main>
<Box component="section" className={classes.hero}>
<Container className={classes.heroGrid}>
<Stack justify="center" gap={0} className={classes.heroCopy}>
<Group gap="md" mb="xl">
<RosettaLogo size={74} />
<Badge size="lg" variant="light" radius="xl">
Private messaging
</Badge>
</Group>
<Title order={1} className={classes.heroTitle}>
A new era of{' '}
<Text
component="span"
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
inherit
>
private
</Text>{' '}
messaging.
</Title>
<Text className={classes.heroDescription} c="dimmed">
Rosetta keeps identity, conversations, and media under your control
— without requiring a phone number or building a readable message
archive on the server.
</Text>
<Group mt={36}>
<Button component="a" href="#apps" size="lg" radius="md">
Download Rosetta
</Button>
<Button
component={Link}
to="/proto"
size="lg"
radius="md"
variant="default"
>
Explore Proto
</Button>
</Group>
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="lg" mt={48}>
{[
['No phone', 'Identity without personal contact data'],
['Encrypted', 'Messages and media protected by default'],
['Local', 'Readable history stays on your devices'],
].map(([value, label]) => (
<div key={value}>
<Text fw={700}>{value}</Text>
<Text c="dimmed" size="xs" mt={4} lh={1.45}>
{label}
</Text>
</div>
))}
</SimpleGrid>
</Stack>
<Box className={classes.preview} aria-label="Rosetta conversation preview">
<Paper withBorder radius={32} shadow="xl" className={classes.phone}>
<Group justify="space-between" className={classes.phoneHeader}>
<Group gap="sm">
<RosettaLogo size={36} />
<div>
<Text fw={700} size="sm">
Rosetta
</Text>
<Text c="blue" size="xs">
Encrypted
</Text>
</div>
</Group>
<ThemeIcon variant="light" radius="xl">
<IconLock size={16} />
</ThemeIcon>
</Group>
<Stack gap="md" className={classes.messages}>
<Paper radius="lg" p="md" className={classes.messageIncoming}>
<Text size="sm">Your recovery phrase never leaves your device.</Text>
<Text c="dimmed" size="xs" ta="right" mt={6}>
10:42
</Text>
</Paper>
<Paper radius="lg" p="md" className={classes.messageOutgoing}>
<Text size="sm">And the server cannot read this conversation.</Text>
<Group justify="flex-end" gap={4} mt={6}>
<Text c="blue" size="xs">
10:43
</Text>
<IconCheck size={14} color="var(--mantine-color-blue-6)" />
</Group>
</Paper>
<Paper radius="lg" p="md" className={classes.messageIncoming}>
<Text size="sm">Exactly. Privacy is the default.</Text>
<Text c="dimmed" size="xs" ta="right" mt={6}>
10:43
</Text>
</Paper>
</Stack>
<Group justify="center" gap="xs" className={classes.encryptionNote}>
<IconKey size={15} />
<Text size="xs">End-to-end encrypted session</Text>
</Group>
</Paper>
<Paper withBorder radius="lg" shadow="md" className={classes.floatingCard}>
<ThemeIcon radius="xl" variant="light">
<IconMessageCircle size={17} />
</ThemeIcon>
<div>
<Text fw={600} size="sm">
Delivered privately
</Text>
<Text c="dimmed" size="xs">
No readable server history
</Text>
</div>
</Paper>
</Box>
</Container>
</Box>
<Box component="section" id="features" className={classes.featuresSection}>
<Container>
<Box className={classes.sectionHeading}>
<Text c="blue" fw={700} size="sm">
Why Rosetta
</Text>
<Title order={2}>Simple to use. Difficult to compromise.</Title>
<Text c="dimmed">
Privacy is built into the account model, transport, and storage
architecture — not added as an optional mode.
</Text>
</Box>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing={42} mt={56}>
{FEATURES.map(({ icon: Icon, title, description }) => (
<div key={title}>
<ThemeIcon size={44} radius="xl" variant="light">
<Icon size={21} stroke={1.7} />
</ThemeIcon>
<Title order={3} className={classes.featureTitle} mt="md">
{title}
</Title>
<Text c="dimmed" size="sm" mt="xs" lh={1.65}>
{description}
</Text>
</div>
))}
</SimpleGrid>
</Container>
</Box>
<DownloadApps />
<Box component="section" className={classes.protocolSection}>
<Container>
<Paper radius="xl" className={classes.protocolCard}>
<Box>
<Text c="blue.2" fw={700} size="sm">
Rosetta Proto
</Text>
<Title order={2} c="white" mt="xs">
The protocol is documented from its source.
</Title>
<Text c="blue.0" mt="md" maw={650} lh={1.65}>
Browse every packet, object, enum, field ID, type reference, and
layer change generated directly from Rosetta YAML schemas.
</Text>
</Box>
<Button
component={Link}
to="/proto"
color="white"
variant="white"
size="lg"
className={classes.protocolButton}
>
Open Proto Documentation
</Button>
</Paper>
</Container>
</Box>
</main>
<SiteFooter />
</>
);
}

View File

@@ -18,7 +18,7 @@ export function PrivacyPage() {
canonical="https://rosetta.im/privacy/"
keywords="Rosetta privacy policy, encrypted messenger privacy, data protection"
/>
<LegalPage active="privacy" title="Privacy Policy" updated="May 22, 2026">
<LegalPage title="Privacy Policy" updated="May 22, 2026">
<LegalText lead>
Rosetta IM is operated by ROSETTA CLOUD SERVICES LIMITED (&quot;Rosetta&quot;,
&quot;we&quot;, &quot;us&quot;, or &quot;our&quot;). This Privacy Policy

View File

@@ -0,0 +1,159 @@
.page {
padding: 48px 0 112px;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.hero {
max-width: 780px;
padding: 64px 0 48px;
}
.hero h1 {
margin-top: var(--mantine-spacing-lg);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: clamp(52px, 7vw, 76px);
font-weight: 600;
letter-spacing: -0.045em;
line-height: 1.02;
}
.heroDescription {
margin-top: var(--mantine-spacing-lg);
font-size: 19px;
line-height: 1.65;
}
.stats {
margin-bottom: 56px;
}
.docsGrid {
display: grid;
grid-template-columns: 200px minmax(0, 1fr);
gap: 48px;
align-items: start;
}
.sidebar {
position: sticky;
top: 104px;
}
.sidebar a {
width: fit-content;
text-decoration: none;
}
.documentation {
min-width: 0;
}
.filters {
display: grid;
gap: var(--mantine-spacing-md);
margin-bottom: 56px;
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
}
.schemaSection {
scroll-margin-top: 104px;
margin-top: 72px;
}
.schemaSection:first-of-type {
margin-top: 0;
}
.schemaSection > h2 {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: 36px;
font-weight: 600;
letter-spacing: -0.03em;
}
.definition {
scroll-margin-top: 104px;
margin-top: 44px;
padding-top: 36px;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.definition h3 {
margin-top: 5px;
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-size: 27px;
font-weight: 600;
letter-spacing: -0.02em;
}
.signature {
margin: var(--mantine-spacing-lg) 0;
padding: 12px 12px 12px 18px;
border: 1px solid light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-9));
border-radius: var(--mantine-radius-md);
background: light-dark(var(--mantine-color-blue-0), rgba(34, 139, 230, 0.1));
}
.signature code {
flex: 1;
padding: 0;
background: transparent;
color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-1));
overflow-wrap: anywhere;
white-space: normal;
}
.table {
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
border-radius: var(--mantine-radius-md);
}
.table th {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
}
.typeLink {
font-family: var(--mantine-font-family-monospace);
font-size: var(--mantine-font-size-sm);
font-weight: 600;
text-decoration: none;
}
.primitive {
color: light-dark(var(--mantine-color-violet-8), var(--mantine-color-violet-3));
}
@media (max-width: 62em) {
.docsGrid {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
padding: var(--mantine-spacing-lg);
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
border-radius: var(--mantine-radius-lg);
}
}
@media (max-width: 48em) {
.page {
padding-top: 32px;
}
.hero {
padding-top: 48px;
}
.docsGrid {
gap: 32px;
}
.schemaSection {
margin-top: 56px;
}
.definition {
margin-top: 36px;
}
}

483
src/pages/ProtoPage.tsx Normal file
View File

@@ -0,0 +1,483 @@
import {
ActionIcon,
Anchor,
Badge,
Box,
Code,
Container,
CopyButton,
Group,
Paper,
SegmentedControl,
Select,
SimpleGrid,
Stack,
Table,
Text,
TextInput,
ThemeIcon,
Title,
Tooltip,
} from '@mantine/core';
import {
IconBraces,
IconCheck,
IconCopy,
IconFileCode,
IconList,
IconPackage,
IconSearch,
} from '@tabler/icons-react';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { SEO } from '../components/SEO/SEO';
import { SiteFooter } from '../components/SiteFooter/SiteFooter';
import { SiteHeader } from '../components/SiteHeader/SiteHeader';
import { protoLayers } from '../generated/protoDocs';
import type {
ProtoEnum,
ProtoField,
ProtoLayer,
ProtoPacket,
ProtoType,
} from '../types/proto';
import classes from './ProtoPage.module.css';
type SchemaCategory = 'all' | 'packets' | 'types' | 'enums';
const PRIMITIVE_TYPES = new Set([
'bool',
'bytes',
'double',
'float',
'int8',
'int16',
'int32',
'int64',
'string',
'uint8',
'uint16',
'uint32',
'uint64',
]);
function slug(value: string) {
return value.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/_/g, '-').toLowerCase();
}
function entityMatches(
entity: ProtoPacket | ProtoType | ProtoEnum,
query: string,
) {
if (!query) {
return true;
}
const searchable = [
entity.name,
entity.description,
'fields' in entity
? entity.fields.map((field) => `${field.name} ${field.type} ${field.description}`).join(' ')
: entity.values.map((value) => `${value.name} ${value.description}`).join(' '),
]
.join(' ')
.toLowerCase();
return searchable.includes(query);
}
function TypeReference({ type, layer }: { type: string; layer: ProtoLayer }) {
const array = type.endsWith('[]');
const baseType = array ? type.slice(0, -2) : type;
const targetType = layer.types.find((item) => item.name === baseType);
const targetEnum = layer.enums.find((item) => item.name === baseType);
if (targetType || targetEnum) {
return (
<Anchor
href={`#${targetType ? 'type' : 'enum'}-${slug(baseType)}`}
className={classes.typeLink}
>
{baseType}
{array && '[]'}
</Anchor>
);
}
if (PRIMITIVE_TYPES.has(baseType)) {
return <Code className={classes.primitive}>{type}</Code>;
}
return <Code>{type}</Code>;
}
function FieldTable({ fields, layer }: { fields: ProtoField[]; layer: ProtoLayer }) {
if (fields.length === 0) {
return (
<Text c="dimmed" size="sm">
This schema has no fields.
</Text>
);
}
return (
<Table.ScrollContainer minWidth={620}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
<Table.Thead>
<Table.Tr>
<Table.Th>ID</Table.Th>
<Table.Th>Field</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Description</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{fields.map((field) => (
<Table.Tr key={`${field.id}-${field.name}`}>
<Table.Td>
<Code>{field.id}</Code>
</Table.Td>
<Table.Td>
<Text component="span" ff="monospace" fw={600} size="sm">
{field.name}
</Text>
{field.optional && (
<Badge size="xs" variant="light" ml="xs">
Optional
</Badge>
)}
</Table.Td>
<Table.Td>
<TypeReference type={field.type} layer={layer} />
</Table.Td>
<Table.Td>
<Text c={field.description ? undefined : 'dimmed'} size="sm">
{field.description || 'No field description in the schema.'}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
);
}
function packetSignature(packet: ProtoPacket) {
const constructor = packet.id.toString(16).padStart(2, '0');
const fields = packet.fields
.map((field) => `${field.name}${field.optional ? '?' : ''}:${field.type}`)
.join(' ');
return `${packet.name}#${constructor}${fields ? ` ${fields}` : ''}`;
}
function typeSignature(type: ProtoType) {
const fields = type.fields
.map((field) => `${field.name}${field.optional ? '?' : ''}:${field.type}`)
.join(' ');
return `${type.name}${fields ? ` ${fields}` : ''} = ${type.name}`;
}
function Signature({ value }: { value: string }) {
return (
<Group gap="xs" wrap="nowrap" className={classes.signature}>
<Code block>{value}</Code>
<CopyButton value={value} timeout={1600}>
{({ copied, copy }) => (
<Tooltip label={copied ? 'Copied' : 'Copy signature'}>
<ActionIcon
onClick={copy}
variant="subtle"
color={copied ? 'teal' : 'blue'}
aria-label="Copy signature"
>
{copied ? <IconCheck size={17} /> : <IconCopy size={17} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
);
}
function PacketDefinition({ packet, layer }: { packet: ProtoPacket; layer: ProtoLayer }) {
return (
<Box component="article" id={`packet-${slug(packet.name)}`} className={classes.definition}>
<Group justify="space-between" align="flex-start">
<div>
<Text c="blue" fw={700} size="xs" tt="uppercase">
Packet · ID {packet.id}
</Text>
<Title order={3}>{packet.name}</Title>
</div>
<Badge variant="light">0x{packet.id.toString(16).padStart(2, '0')}</Badge>
</Group>
<Text c="dimmed" mt="sm" lh={1.65}>
{packet.description || `Packet ${packet.name} in protocol layer ${layer.version}.`}
</Text>
<Signature value={packetSignature(packet)} />
<FieldTable fields={packet.fields} layer={layer} />
</Box>
);
}
function TypeDefinition({ type, layer }: { type: ProtoType; layer: ProtoLayer }) {
return (
<Box component="article" id={`type-${slug(type.name)}`} className={classes.definition}>
<Text c="blue" fw={700} size="xs" tt="uppercase">
Object
</Text>
<Title order={3}>{type.name}</Title>
<Text c="dimmed" mt="sm" lh={1.65}>
{type.description || `Object ${type.name} in protocol layer ${layer.version}.`}
</Text>
<Signature value={typeSignature(type)} />
<FieldTable fields={type.fields} layer={layer} />
</Box>
);
}
function EnumDefinition({ item, layer }: { item: ProtoEnum; layer: ProtoLayer }) {
return (
<Box component="article" id={`enum-${slug(item.name)}`} className={classes.definition}>
<Text c="blue" fw={700} size="xs" tt="uppercase">
Enum · {item.type}
</Text>
<Title order={3}>{item.name}</Title>
<Text c="dimmed" mt="sm" lh={1.65}>
{item.description || `Enumeration ${item.name} in protocol layer ${layer.version}.`}
</Text>
<Signature value={`${item.name}:${item.type}`} />
<Table.ScrollContainer minWidth={520}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
<Table.Thead>
<Table.Tr>
<Table.Th>Value</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Description</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{item.values.map((value) => (
<Table.Tr key={`${value.id}-${value.name}`}>
<Table.Td>
<Code>{value.id}</Code>
</Table.Td>
<Table.Td>
<Text ff="monospace" fw={600} size="sm">
{value.name}
</Text>
</Table.Td>
<Table.Td>
<Text c={value.description ? undefined : 'dimmed'} size="sm">
{value.description || 'No value description in the schema.'}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
</Box>
);
}
export function ProtoPage() {
const latestLayer = protoLayers.at(-1)!;
const [selectedVersion, setSelectedVersion] = useState(String(latestLayer.version));
const [category, setCategory] = useState<SchemaCategory>('all');
const [search, setSearch] = useState('');
const layer =
protoLayers.find((item) => String(item.version) === selectedVersion) || latestLayer;
const query = search.trim().toLowerCase();
const packets = layer.packets.filter((packet) => entityMatches(packet, query));
const types = layer.types.filter((type) => entityMatches(type, query));
const enums = layer.enums.filter((item) => entityMatches(item, query));
const resultCount =
(category === 'all' || category === 'packets' ? packets.length : 0) +
(category === 'all' || category === 'types' ? types.length : 0) +
(category === 'all' || category === 'enums' ? enums.length : 0);
return (
<>
<SEO
title="Rosetta Proto Documentation"
description="Generated Rosetta protocol documentation for packets, objects, enums, fields, and schema layers."
canonical="https://rosetta.im/proto/"
keywords="Rosetta protocol, packet schema, YAML documentation, Proto"
/>
<SiteHeader />
<main className={classes.page}>
<Container>
<Anchor component={Link} to="/" size="sm">
Rosetta
</Anchor>
<Text component="span" c="dimmed" size="sm">
{' '}
/ Proto
</Text>
<Box className={classes.hero}>
<Group gap="sm">
<ThemeIcon size={46} radius="md" variant="light">
<IconFileCode size={24} />
</ThemeIcon>
<Badge variant="light">Generated from YAML</Badge>
</Group>
<Title order={1}>Rosetta Proto</Title>
<Text c="dimmed" className={classes.heroDescription}>
Complete protocol reference generated from the RCC layer schemas. Browse
packet constructors, objects, enumerations, field IDs, optional values,
comments, and linked types.
</Text>
</Box>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md" className={classes.stats}>
{[
[IconPackage, layer.packets.length, 'Packets'],
[IconBraces, layer.types.length, 'Objects'],
[IconList, layer.enums.length, 'Enums'],
].map(([Icon, value, label]) => {
const StatIcon = Icon as typeof IconPackage;
return (
<Paper key={String(label)} withBorder radius="md" p="lg">
<Group>
<ThemeIcon variant="light" radius="xl">
<StatIcon size={18} />
</ThemeIcon>
<div>
<Text fw={700} fz="xl">
{String(value)}
</Text>
<Text c="dimmed" size="sm">
{String(label)}
</Text>
</div>
</Group>
</Paper>
);
})}
</SimpleGrid>
<Box className={classes.docsGrid}>
<aside className={classes.sidebar}>
<Stack gap="lg">
<Select
label="Protocol layer"
value={selectedVersion}
onChange={(value) => value && setSelectedVersion(value)}
data={protoLayers.map((item) => ({
value: String(item.version),
label: `Layer ${item.version}${item === latestLayer ? ' · latest' : ''}`,
}))}
/>
<Stack gap={6}>
<Text fw={700} size="xs" tt="uppercase" c="dimmed">
Contents
</Text>
<Anchor href="#packets" size="sm">
Packets ({layer.packets.length})
</Anchor>
<Anchor href="#objects" size="sm">
Objects ({layer.types.length})
</Anchor>
<Anchor href="#enums" size="sm">
Enums ({layer.enums.length})
</Anchor>
</Stack>
<Stack gap={6}>
<Text fw={700} size="xs" tt="uppercase" c="dimmed">
Source
</Text>
<Code>{layer.source}</Code>
<Text c="dimmed" size="xs" lh={1.5}>
{layer.description || 'Initial protocol schema.'}
</Text>
</Stack>
</Stack>
</aside>
<Box className={classes.documentation}>
<Paper withBorder radius="lg" p="lg" className={classes.filters}>
<TextInput
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch size={17} />}
placeholder="Search packets, objects, fields, or types"
aria-label="Search Proto documentation"
/>
<SegmentedControl
value={category}
onChange={(value) => setCategory(value as SchemaCategory)}
data={[
{ label: 'All', value: 'all' },
{ label: 'Packets', value: 'packets' },
{ label: 'Objects', value: 'types' },
{ label: 'Enums', value: 'enums' },
]}
fullWidth
/>
<Text c="dimmed" size="xs">
{resultCount} definitions in layer {layer.version}
</Text>
</Paper>
{(category === 'all' || category === 'packets') && packets.length > 0 && (
<Box component="section" id="packets" className={classes.schemaSection}>
<Title order={2}>Packets</Title>
<Text c="dimmed" mt="xs">
Network packet constructors and their serialized fields.
</Text>
{packets.map((packet) => (
<PacketDefinition
key={packet.name}
packet={packet}
layer={layer}
/>
))}
</Box>
)}
{(category === 'all' || category === 'types') && types.length > 0 && (
<Box component="section" id="objects" className={classes.schemaSection}>
<Title order={2}>Objects</Title>
<Text c="dimmed" mt="xs">
Reusable structured values referenced by packet fields.
</Text>
{types.map((type) => (
<TypeDefinition key={type.name} type={type} layer={layer} />
))}
</Box>
)}
{(category === 'all' || category === 'enums') && enums.length > 0 && (
<Box component="section" id="enums" className={classes.schemaSection}>
<Title order={2}>Enums</Title>
<Text c="dimmed" mt="xs">
Named protocol constants and their serialized numeric values.
</Text>
{enums.map((item) => (
<EnumDefinition key={item.name} item={item} layer={layer} />
))}
</Box>
)}
{resultCount === 0 && (
<Paper withBorder radius="lg" p="xl" ta="center">
<Text fw={600}>No schema definitions found.</Text>
<Text c="dimmed" size="sm" mt="xs">
Try another search term or category.
</Text>
</Paper>
)}
</Box>
</Box>
</Container>
</main>
<SiteFooter />
</>
);
}

View File

@@ -161,7 +161,6 @@ export function SupportPage() {
keywords="Rosetta support, account recovery, messaging help, privacy help"
/>
<LegalPage
active="support"
eyebrow="Rosetta Support"
title="Help with your Rosetta account and messages."
>

View File

@@ -18,7 +18,7 @@ export function TermsPage() {
canonical="https://rosetta.im/terms/"
keywords="Rosetta terms of use, end user license agreement, encrypted messenger terms"
/>
<LegalPage active="terms" title="Terms of Use" updated="May 22, 2026">
<LegalPage title="Terms of Use" updated="May 22, 2026">
<LegalText lead>
These Terms of Use and End User License Agreement (&quot;Terms&quot;) govern
your use of Rosetta, an end-to-end encrypted messaging application operated by

42
src/services/updates.ts Normal file
View File

@@ -0,0 +1,42 @@
export interface DownloadRelease {
platform: string;
arch: string;
version: string;
link: string;
}
interface UpdateItem {
platform: string;
arch: string;
version: string;
downloadUrl: string;
}
const PLATFORM_ORDER = ['linux', 'win32', 'android'];
const ARCH_ORDER = ['arm64', 'x64', 'universal'];
export async function fetchUpdates(signal?: AbortSignal): Promise<DownloadRelease[]> {
const response = await fetch('https://sdu.rosetta.im/updates/all', { signal });
if (!response.ok) {
throw new Error(`Update server returned ${response.status}`);
}
const data = (await response.json()) as { items?: UpdateItem[] };
return (data.items || [])
.map((item) => ({
platform: item.platform,
arch: item.arch,
version: item.version,
link: new URL(item.downloadUrl, 'https://sdu.rosetta.im').toString(),
}))
.sort((a, b) => {
const platformComparison =
PLATFORM_ORDER.indexOf(a.platform) - PLATFORM_ORDER.indexOf(b.platform);
if (platformComparison !== 0) {
return platformComparison;
}
return ARCH_ORDER.indexOf(a.arch) - ARCH_ORDER.indexOf(b.arch);
});
}

View File

@@ -1,3 +1,22 @@
*{
scroll-behavior: smooth;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
min-width: 320px;
margin: 0;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
a {
text-underline-offset: 3px;
}
::selection {
background: var(--mantine-color-blue-1);
color: var(--mantine-color-blue-9);
}

42
src/types/proto.ts Normal file
View File

@@ -0,0 +1,42 @@
export interface ProtoField {
id: number;
name: string;
type: string;
optional: boolean;
description: string;
}
export interface ProtoPacket {
name: string;
id: number;
description: string;
fields: ProtoField[];
}
export interface ProtoType {
name: string;
description: string;
fields: ProtoField[];
}
export interface ProtoEnumValue {
id: number;
name: string;
description: string;
}
export interface ProtoEnum {
name: string;
type: string;
description: string;
values: ProtoEnumValue[];
}
export interface ProtoLayer {
version: number;
description: string;
source: string;
enums: ProtoEnum[];
types: ProtoType[];
packets: ProtoPacket[];
}