forked from Rosetta/landing
Compare commits
10 Commits
e8c221bda4
...
b96ad82cf7
| Author | SHA1 | Date | |
|---|---|---|---|
| b96ad82cf7 | |||
| 5278509046 | |||
| 026f69b179 | |||
| 94bc883205 | |||
| 12f6384856 | |||
| bff945e701 | |||
| 354c8e5489 | |||
|
|
4bc65f71cf | ||
| aea0b94598 | |||
| 7af3c46bfe |
@@ -1,11 +1,37 @@
|
||||
import { copyFile, mkdir } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const root = process.cwd();
|
||||
const outputDirectory = resolve(root, 'dist');
|
||||
const serverDirectory = resolve(root, 'dist', 'server');
|
||||
const appShell = resolve(outputDirectory, 'index.html');
|
||||
const browserRoutes = [
|
||||
'encryption',
|
||||
'privacy',
|
||||
'proto',
|
||||
'proto/docs',
|
||||
'rcc',
|
||||
'support',
|
||||
'terms',
|
||||
];
|
||||
|
||||
await mkdir(serverDirectory, { recursive: true });
|
||||
await copyFile(
|
||||
resolve(root, 'worker', 'index.js'),
|
||||
resolve(serverDirectory, 'index.js'),
|
||||
);
|
||||
|
||||
// Emit both clean-URL and directory-index shells for every client-side route.
|
||||
// The directory copy also supports basic Nginx static configurations that do
|
||||
// not rewrite missing paths to the root application shell.
|
||||
await Promise.all(
|
||||
browserRoutes.map(async (route) => {
|
||||
const routeFile = resolve(outputDirectory, `${route}.html`);
|
||||
const routeDirectory = resolve(outputDirectory, route);
|
||||
|
||||
await mkdir(dirname(routeFile), { recursive: true });
|
||||
await mkdir(routeDirectory, { recursive: true });
|
||||
await copyFile(appShell, routeFile);
|
||||
await copyFile(appShell, resolve(routeDirectory, 'index.html'));
|
||||
}),
|
||||
);
|
||||
|
||||
19
nginx.conf
Normal file
19
nginx.conf
Normal file
@@ -0,0 +1,19 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location /assets/ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location / {
|
||||
add_header Cache-Control "no-cache" always;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -2,57 +2,56 @@ version: 3
|
||||
description: Add avatars and more CDN safety, remove transport server
|
||||
|
||||
enums:
|
||||
#Тип вложния
|
||||
#тип вложния
|
||||
AttachmentType:
|
||||
type: int8
|
||||
values:
|
||||
- id: 0
|
||||
name: IMAGE #Изображение
|
||||
name: IMAGE #изображение
|
||||
- id: 1
|
||||
name: MESSAGES #Сообщения (forwarded/reply)
|
||||
name: MESSAGES #сообщения (forwarded/reply)
|
||||
- id: 2
|
||||
name: FILE #Файл
|
||||
name: FILE #файл
|
||||
- id: 4
|
||||
name: CALL #Звонок
|
||||
name: CALL #звонок
|
||||
- id: 5
|
||||
name: VOICE #Голосовое сообщение
|
||||
name: VOICE #голосовое сообщение
|
||||
- id: 6
|
||||
name: VIDEO_CIRCLE #Видео сообщение (кружок)
|
||||
name: VIDEO_CIRCLE #видео сообщение (кружок)
|
||||
- id: 7
|
||||
name: VIDEO #Видео
|
||||
name: VIDEO #видео
|
||||
|
||||
#Решение по девайсу, принять или отклонить вход
|
||||
#решение по девайсу, принять или отклонить вход
|
||||
DeviceSolution:
|
||||
type: int8
|
||||
values:
|
||||
- id: 0
|
||||
name: ACCEPT #Принять
|
||||
name: ACCEPT #принять
|
||||
- id: 1
|
||||
name: DECLINE #Отклонить
|
||||
name: DECLINE #отклонить
|
||||
|
||||
#Статус хэндшейка
|
||||
#статус хэндшейка
|
||||
HandshakeStage:
|
||||
type: int8
|
||||
values:
|
||||
- id: 0
|
||||
name: COMPLETED #Пройден
|
||||
name: COMPLETED #пройден
|
||||
- id: 1
|
||||
name: NEED_DEVICE_VERIFICATION #Не пройден (приложение рендерит UI подтвердите с другого устройства)
|
||||
name: NEED_DEVICE_VERIFICATION #не пройден (приложение рендерит UI подтвердите с другого устройства)
|
||||
|
||||
#Статус в группе
|
||||
#статус в группе
|
||||
GroupStatus:
|
||||
type: int8
|
||||
values:
|
||||
- id: 0
|
||||
name: JOINED #В группе
|
||||
name: JOINED #в группе
|
||||
- id: 1
|
||||
name: INVALID #Не валидная группа
|
||||
name: INVALID #не валидная группа
|
||||
- id: 2
|
||||
name: NOT_JOINED #Можно зайт
|
||||
name: NOT_JOINED #можно зайт
|
||||
- id: 3
|
||||
name: BANNED #Заблокирован
|
||||
name: BANNED #заблокирован
|
||||
|
||||
#
|
||||
NotificationAction:
|
||||
type: int8
|
||||
values:
|
||||
|
||||
@@ -8,10 +8,16 @@
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://rosetta.im/proto/</loc>
|
||||
<lastmod>2026-07-30</lastmod>
|
||||
<lastmod>2026-08-01</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://rosetta.im/rcc/</loc>
|
||||
<lastmod>2026-08-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://rosetta.im/proto/docs/</loc>
|
||||
<lastmod>2026-07-31</lastmod>
|
||||
|
||||
@@ -18,14 +18,20 @@ async function exists(path) {
|
||||
}
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(value) {
|
||||
return value.trim().replace(/\p{L}/u, (letter) => letter.toLocaleUpperCase());
|
||||
}
|
||||
|
||||
function cleanComment(...comments) {
|
||||
return comments
|
||||
const description = comments
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.split('\n')
|
||||
.map((line) => line.replace(/^\s*#?\s?/, '').trim())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return capitalizeFirstLetter(description);
|
||||
}
|
||||
|
||||
function mapFields(node) {
|
||||
@@ -128,7 +134,7 @@ async function readLayer(path) {
|
||||
|
||||
return {
|
||||
version: Number(document.get('version')),
|
||||
description: String(document.get('description') || ''),
|
||||
description: capitalizeFirstLetter(String(document.get('description') || '')),
|
||||
source: basename(path),
|
||||
enums: mapNamedCollection(document.get('enums', true), 'enum'),
|
||||
types: mapNamedCollection(document.get('types', true), 'type'),
|
||||
|
||||
41
src/App.tsx
41
src/App.tsx
@@ -1,7 +1,7 @@
|
||||
import '@mantine/core/styles.css';
|
||||
|
||||
import { Center, createTheme, Loader, MantineProvider } from '@mantine/core';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Button, Center, createTheme, Loader, MantineProvider, Stack, Text, Title } from '@mantine/core';
|
||||
import { Component, lazy, Suspense, type ReactNode } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ScrollToTop } from './components/ScrollToTop/ScrollToTop';
|
||||
import './style.css';
|
||||
@@ -18,6 +18,9 @@ const ProtoPage = lazy(() =>
|
||||
const ProtoDocsPage = lazy(() =>
|
||||
import('./pages/ProtoPage').then((module) => ({ default: module.ProtoDocsPage })),
|
||||
);
|
||||
const RccPage = lazy(() =>
|
||||
import('./pages/RccPage').then((module) => ({ default: module.RccPage })),
|
||||
);
|
||||
const SupportPage = lazy(() =>
|
||||
import('./pages/SupportPage').then((module) => ({ default: module.SupportPage })),
|
||||
);
|
||||
@@ -39,9 +42,41 @@ const theme = createTheme({
|
||||
},
|
||||
});
|
||||
|
||||
class PageErrorBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
{ hasError: boolean }
|
||||
> {
|
||||
state = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Center mih="100vh" p="xl">
|
||||
<Stack align="center" gap="md" maw={460}>
|
||||
<Title order={2} ta="center">
|
||||
This page could not be loaded
|
||||
</Title>
|
||||
<Text c="dimmed" ta="center">
|
||||
Rosetta may have been updated while this tab was open. Reload the page to continue.
|
||||
</Text>
|
||||
<Button onClick={() => window.location.reload()}>Reload page</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<MantineProvider theme={theme}>
|
||||
<PageErrorBoundary>
|
||||
<ScrollToTop />
|
||||
<Suspense
|
||||
fallback={
|
||||
@@ -55,11 +90,13 @@ export default function App() {
|
||||
<Route path="/encryption" element={<EncryptionPage />} />
|
||||
<Route path="/proto" element={<ProtoPage />} />
|
||||
<Route path="/proto/docs" element={<ProtoDocsPage />} />
|
||||
<Route path="/rcc" element={<RccPage />} />
|
||||
<Route path="/support" element={<SupportPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPage />} />
|
||||
<Route path="/terms" element={<TermsPage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</PageErrorBoundary>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 100vh;
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
|
||||
}
|
||||
|
||||
.shell {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-top: 72px;
|
||||
padding-bottom: 96px;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 900px;
|
||||
margin-inline: auto;
|
||||
padding: clamp(30px, 5vw, 60px);
|
||||
@@ -21,6 +25,7 @@
|
||||
}
|
||||
|
||||
.intro {
|
||||
min-width: 0;
|
||||
max-width: 760px;
|
||||
padding-bottom: 36px;
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
@@ -69,6 +74,7 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
min-width: 0;
|
||||
margin-top: 44px;
|
||||
padding-top: 36px;
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
|
||||
@@ -21,6 +21,7 @@ interface LegalPageProps {
|
||||
interface LegalSectionProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
anchorId?: string;
|
||||
}
|
||||
|
||||
export function LegalPage({
|
||||
@@ -58,9 +59,9 @@ export function LegalPage({
|
||||
);
|
||||
}
|
||||
|
||||
export function LegalSection({ title, children }: LegalSectionProps) {
|
||||
export function LegalSection({ title, children, anchorId }: LegalSectionProps) {
|
||||
return (
|
||||
<Box component="section" className={classes.section}>
|
||||
<Box component="section" id={anchorId} className={classes.section}>
|
||||
<Title order={2} className={classes.sectionTitle}>
|
||||
{title}
|
||||
</Title>
|
||||
|
||||
@@ -5,15 +5,42 @@ export function ScrollToTop() {
|
||||
const { pathname, hash } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
if (hash) {
|
||||
const target = document.getElementById(decodeURIComponent(hash.slice(1)));
|
||||
if (target) {
|
||||
target.scrollIntoView();
|
||||
return;
|
||||
}
|
||||
let targetId: string;
|
||||
|
||||
try {
|
||||
targetId = decodeURIComponent(hash.slice(1));
|
||||
} catch {
|
||||
targetId = hash.slice(1);
|
||||
}
|
||||
|
||||
const scrollToTarget = () => {
|
||||
const target = document.getElementById(targetId);
|
||||
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
target.scrollIntoView({ block: 'start' });
|
||||
return true;
|
||||
};
|
||||
|
||||
if (scrollToTarget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (scrollToTarget()) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export function SiteFooter() {
|
||||
return (
|
||||
<footer className={classes.footer}>
|
||||
<Container>
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl">
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="xl">
|
||||
<Stack gap="sm">
|
||||
<Group gap="sm">
|
||||
<RosettaLogo size={34} />
|
||||
@@ -33,14 +33,23 @@ export function SiteFooter() {
|
||||
<Anchor component={Link} to="/#apps" c="dimmed" size="sm">
|
||||
Apps
|
||||
</Anchor>
|
||||
<Anchor component={Link} to="/encryption" c="dimmed" size="sm">
|
||||
Encryption
|
||||
<Anchor component={Link} to="/support" c="dimmed" size="sm">
|
||||
Support
|
||||
</Anchor>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} size="sm">
|
||||
Developers
|
||||
</Text>
|
||||
<Anchor component={Link} to="/proto" c="dimmed" size="sm">
|
||||
Proto
|
||||
</Anchor>
|
||||
<Anchor component={Link} to="/support" c="dimmed" size="sm">
|
||||
Support
|
||||
<Anchor component={Link} to="/encryption" c="dimmed" size="sm">
|
||||
Encryption
|
||||
</Anchor>
|
||||
<Anchor component={Link} to="/rcc" c="dimmed" size="sm">
|
||||
RCC
|
||||
</Anchor>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -2402,101 +2402,101 @@ export const protoLayers = [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "IMAGE",
|
||||
"description": ""
|
||||
"description": "Изображение"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "MESSAGES",
|
||||
"description": ""
|
||||
"description": "Сообщения (forwarded/reply)"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "FILE",
|
||||
"description": ""
|
||||
"description": "Файл"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "CALL",
|
||||
"description": ""
|
||||
"description": "Звонок"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "VOICE",
|
||||
"description": ""
|
||||
"description": "Голосовое сообщение"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "VIDEO_CIRCLE",
|
||||
"description": ""
|
||||
"description": "Видео сообщение (кружок)"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "VIDEO",
|
||||
"description": ""
|
||||
"description": "Видео"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DeviceSolution",
|
||||
"type": "int8",
|
||||
"description": "",
|
||||
"description": "Решение по девайсу, принять или отклонить вход",
|
||||
"yaml": "DeviceSolution:\n type: int8\n values:\n - id: 0\n name: ACCEPT\n - id: 1\n name: DECLINE",
|
||||
"values": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "ACCEPT",
|
||||
"description": ""
|
||||
"description": "Принять"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "DECLINE",
|
||||
"description": ""
|
||||
"description": "Отклонить"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HandshakeStage",
|
||||
"type": "int8",
|
||||
"description": "",
|
||||
"description": "Статус хэндшейка",
|
||||
"yaml": "HandshakeStage:\n type: int8\n values:\n - id: 0\n name: COMPLETED\n - id: 1\n name: NEED_DEVICE_VERIFICATION",
|
||||
"values": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "COMPLETED",
|
||||
"description": ""
|
||||
"description": "Пройден"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "NEED_DEVICE_VERIFICATION",
|
||||
"description": ""
|
||||
"description": "Не пройден (приложение рендерит UI подтвердите с другого устройства)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GroupStatus",
|
||||
"type": "int8",
|
||||
"description": "",
|
||||
"description": "Статус в группе",
|
||||
"yaml": "GroupStatus:\n type: int8\n values:\n - id: 0\n name: JOINED\n - id: 1\n name: INVALID\n - id: 2\n name: NOT_JOINED\n - id: 3\n name: BANNED",
|
||||
"values": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "JOINED",
|
||||
"description": ""
|
||||
"description": "В группе"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "INVALID",
|
||||
"description": ""
|
||||
"description": "Не валидная группа"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "NOT_JOINED",
|
||||
"description": ""
|
||||
"description": "Можно зайт"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "BANNED",
|
||||
"description": ""
|
||||
"description": "Заблокирован"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2615,23 +2615,23 @@ export const protoLayers = [
|
||||
{
|
||||
"name": "WebRTCType",
|
||||
"type": "int8",
|
||||
"description": "",
|
||||
"description": "Тип WebRTC сигнала",
|
||||
"yaml": "WebRTCType:\n type: int8\n values:\n - id: 0\n name: OFFER\n - id: 1\n name: ANSWER\n - id: 2\n name: ICE_CANDIDATE",
|
||||
"values": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "OFFER",
|
||||
"description": ""
|
||||
"description": "Offer"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ANSWER",
|
||||
"description": ""
|
||||
"description": "Answer"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "ICE_CANDIDATE",
|
||||
"description": ""
|
||||
"description": "Estabilishment candidate"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2654,22 +2654,22 @@ export const protoLayers = [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "INVALID",
|
||||
"description": "любая ошибка смысл которой зависит от контекста, например в случае с файловой системой - нет файла"
|
||||
"description": "Любая ошибка смысл которой зависит от контекста, например в случае с файловой системой - нет файла"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "USERNAME_TAKEN",
|
||||
"description": ""
|
||||
"description": "Имя пользователя занято"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "OVERSIZE",
|
||||
"description": "слишком большой размер файла"
|
||||
"description": "Слишком большой размер файла"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "OVERLIFE",
|
||||
"description": "слишком большой TTL"
|
||||
"description": "Слишком большой TTL"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2716,35 +2716,35 @@ export const protoLayers = [
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": ""
|
||||
"description": "Случайная строка"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "blob",
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": ""
|
||||
"description": "Только очень коротких типов"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "type",
|
||||
"type": "AttachmentType",
|
||||
"optional": false,
|
||||
"description": ""
|
||||
"description": "Тип вложения"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "preview",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": ""
|
||||
"description": "Превью"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "objectId",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": ""
|
||||
"description": "Выданный сервером objectId от cdn"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2758,35 +2758,35 @@ export const protoLayers = [
|
||||
"name": "preview",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "превью (для фото blurhash)"
|
||||
"description": "Превью (для фото blurhash)"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "objectId",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "обьект на cdn"
|
||||
"description": "Обьект на cdn"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "type",
|
||||
"type": "ProfileMediaType",
|
||||
"optional": false,
|
||||
"description": "тип медиа"
|
||||
"description": "Тип медиа"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "timestamp",
|
||||
"type": "int64",
|
||||
"optional": false,
|
||||
"description": "дата публикации"
|
||||
"description": "Дата публикации"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "mediaKey",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "ключ шифрования от медиа"
|
||||
"description": "Ключ шифрования от медиа"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2863,42 +2863,42 @@ export const protoLayers = [
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "имя для поиска"
|
||||
"description": "Имя для поиска"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "отображаемое имя пользователя"
|
||||
"description": "Отображаемое имя пользователя"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "publicKey",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "публичный ключ"
|
||||
"description": "Публичный ключ"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "verified",
|
||||
"type": "int32",
|
||||
"optional": false,
|
||||
"description": "верификация"
|
||||
"description": "Верификация"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "networkStatus",
|
||||
"type": "NetworkStatus",
|
||||
"optional": false,
|
||||
"description": "онлайн пользователь или нет"
|
||||
"description": "Онлайн пользователь или нет"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "avatar",
|
||||
"type": "ProfileMedia",
|
||||
"optional": true,
|
||||
"description": "аватар пользователя если есть"
|
||||
"description": "Аватар пользователя если есть"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2912,42 +2912,42 @@ export const protoLayers = [
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "имя для поиска"
|
||||
"description": "Имя для поиска"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "отображаемое имя пользователя"
|
||||
"description": "Отображаемое имя пользователя"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "publicKey",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "публичный ключ"
|
||||
"description": "Публичный ключ"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "verified",
|
||||
"type": "int32",
|
||||
"optional": false,
|
||||
"description": "верификация"
|
||||
"description": "Верификация"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "networkStatus",
|
||||
"type": "NetworkStatus",
|
||||
"optional": false,
|
||||
"description": "онлайн пользователь или нет"
|
||||
"description": "Онлайн пользователь или нет"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "media",
|
||||
"type": "ProfileMedia[]",
|
||||
"optional": true,
|
||||
"description": "все активное медиа (включая будущие истории)"
|
||||
"description": "Все активное медиа (включая будущие истории)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3361,7 +3361,7 @@ export const protoLayers = [
|
||||
"name": "server",
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "выбранный транспортный сервер"
|
||||
"description": "Выбранный транспортный сервер"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3690,42 +3690,42 @@ export const protoLayers = [
|
||||
"name": "size",
|
||||
"type": "uint32",
|
||||
"optional": false,
|
||||
"description": "размер загружаемого файла (декларируемый)"
|
||||
"description": "Размер загружаемого файла (декларируемый)"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "objectId",
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "выделенный сервером objectId"
|
||||
"description": "Выделенный сервером objectId"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "chunkLength",
|
||||
"type": "uint32",
|
||||
"optional": true,
|
||||
"description": "размер чанка"
|
||||
"description": "Размер чанка"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "chunks",
|
||||
"type": "uint32",
|
||||
"optional": true,
|
||||
"description": "количество чанков"
|
||||
"description": "Количество чанков"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "host",
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "хост, где сохранен файл"
|
||||
"description": "Хост, где сохранен файл"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "ttl",
|
||||
"type": "uint32",
|
||||
"optional": true,
|
||||
"description": "ttl в секундах, после которого файл будет удален с CDN и с таблицы, по умолчанию 7 дней, не допускается использование слишком больших значений"
|
||||
"description": "Ttl в секундах, после которого файл будет удален с CDN и с таблицы, по умолчанию 7 дней, не допускается использование слишком больших значений"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3740,35 +3740,35 @@ export const protoLayers = [
|
||||
"name": "size",
|
||||
"type": "uint32",
|
||||
"optional": true,
|
||||
"description": "размер файла"
|
||||
"description": "Размер файла"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "objectId",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "выделенный сервером objectId"
|
||||
"description": "Выделенный сервером objectId"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "chunkLength",
|
||||
"type": "uint32",
|
||||
"optional": true,
|
||||
"description": "размер чанка"
|
||||
"description": "Размер чанка"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "chunks",
|
||||
"type": "uint32",
|
||||
"optional": true,
|
||||
"description": "количество чанков"
|
||||
"description": "Количество чанков"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "host",
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "сервер с которого скачивать файл"
|
||||
"description": "Сервер с которого скачивать файл"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3783,21 +3783,21 @@ export const protoLayers = [
|
||||
"name": "objectId",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "выделенный сервером objectId при отправке пакета UploadCandidate"
|
||||
"description": "Выделенный сервером objectId при отправке пакета UploadCandidate"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "blurhash",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "превью аватара, строка blurhash"
|
||||
"description": "Превью аватара, строка blurhash"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "mediaKey",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "ключ шифрования"
|
||||
"description": "Ключ шифрования"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3812,7 +3812,7 @@ export const protoLayers = [
|
||||
"name": "objectId",
|
||||
"type": "string",
|
||||
"optional": false,
|
||||
"description": "objectId аватара, аватар или фотография профиля должна при этом стоять у профиля отправителя пакета"
|
||||
"description": "ObjectId аватара, аватар или фотография профиля должна при этом стоять у профиля отправителя пакета"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
36
src/main.tsx
36
src/main.tsx
@@ -2,6 +2,42 @@ import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App.tsx'
|
||||
|
||||
const PRELOAD_RELOAD_KEY = 'rosetta:preload-reload-at'
|
||||
const PRELOAD_RELOAD_COOLDOWN = 10_000
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
window.addEventListener('vite:preloadError', (event) => {
|
||||
let lastReload = 0
|
||||
|
||||
try {
|
||||
lastReload = Number(window.sessionStorage.getItem(PRELOAD_RELOAD_KEY) || 0)
|
||||
} catch {
|
||||
// Storage can be unavailable in restricted browser modes.
|
||||
}
|
||||
|
||||
if (Date.now() - lastReload < PRELOAD_RELOAD_COOLDOWN) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
try {
|
||||
window.sessionStorage.setItem(PRELOAD_RELOAD_KEY, String(Date.now()))
|
||||
} catch {
|
||||
// Reload recovery still works when storage is unavailable.
|
||||
}
|
||||
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
window.sessionStorage.removeItem(PRELOAD_RELOAD_KEY)
|
||||
} catch {
|
||||
// Nothing to clear when storage is unavailable.
|
||||
}
|
||||
}, PRELOAD_RELOAD_COOLDOWN)
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
|
||||
.callout {
|
||||
margin: 18px 0 26px;
|
||||
border: 1px solid light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-9));
|
||||
border-left: 3px solid var(--mantine-color-blue-6);
|
||||
background: light-dark(var(--mantine-color-blue-0), rgba(25, 113, 194, 0.12));
|
||||
border: 1px solid light-dark(var(--mantine-color-red-3), var(--mantine-color-red-9));
|
||||
border-left: 3px solid var(--mantine-color-red-6);
|
||||
background: light-dark(var(--mantine-color-red-0), rgba(250, 82, 82, 0.1));
|
||||
}
|
||||
|
||||
.vectorGroup {
|
||||
@@ -58,17 +58,21 @@
|
||||
}
|
||||
|
||||
.marker {
|
||||
padding: 1px 3px;
|
||||
background: light-dark(var(--mantine-color-blue-1), rgba(25, 113, 194, 0.24));
|
||||
color: inherit;
|
||||
padding: 0 0.14em;
|
||||
border-radius: 2px;
|
||||
background: light-dark(rgba(250, 82, 82, 0.13), rgba(250, 82, 82, 0.2));
|
||||
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-2));
|
||||
font-weight: inherit;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin: 18px 0;
|
||||
padding: 12px 14px;
|
||||
border-left: 3px solid var(--mantine-color-blue-6);
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-2));
|
||||
border-left: 3px solid var(--mantine-color-red-6);
|
||||
background: light-dark(var(--mantine-color-red-0), rgba(250, 82, 82, 0.1));
|
||||
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-2));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 650;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -185,10 +185,11 @@ keyEnvelope = Base64(iv || AES-CBC(wrappingKey, messageSecret) || ephemeral.publ
|
||||
</Box>
|
||||
<LegalText>
|
||||
The result contains 13 encrypted content bytes followed by the 16-byte
|
||||
Poly1305 authentication tag.{' '}
|
||||
Poly1305 authentication tag. Any change to either part causes{' '}
|
||||
<Text component="span" className={classes.marker}>
|
||||
Any change to either part causes authenticated decryption to fail.
|
||||
authenticated decryption to fail
|
||||
</Text>
|
||||
.
|
||||
</LegalText>
|
||||
|
||||
<Title order={3} className={classes.subheading}>
|
||||
|
||||
@@ -126,6 +126,14 @@ export function PrivacyPage() {
|
||||
some requests may require you to provide information that helps us verify
|
||||
control of the relevant account or keys.
|
||||
</LegalText>
|
||||
<LegalText>
|
||||
For step-by-step deletion instructions, including what data may remain, see
|
||||
the{' '}
|
||||
<Anchor component={Link} to="/terms#account-deletion">
|
||||
Account Deletion section of the Terms of Use
|
||||
</Anchor>
|
||||
.
|
||||
</LegalText>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection title="8. Children">
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.protoCode {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
margin: 18px 0;
|
||||
padding: 16px;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
@@ -23,18 +26,37 @@
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.important {
|
||||
padding: 0 0.14em;
|
||||
border-radius: 2px;
|
||||
background: light-dark(rgba(250, 82, 82, 0.13), rgba(250, 82, 82, 0.2));
|
||||
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-2));
|
||||
font-weight: inherit;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.layerExamples {
|
||||
min-width: 0;
|
||||
margin: 18px 0 24px;
|
||||
}
|
||||
|
||||
.layerExamples > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.layerExamples .protoCode {
|
||||
height: calc(100% - 30px);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.docsLinks {
|
||||
margin-top: 44px;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.docsLink {
|
||||
display: inline-block;
|
||||
margin-top: 44px;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
@@ -51,6 +73,8 @@
|
||||
|
||||
.docsGrid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
gap: 44px;
|
||||
align-items: start;
|
||||
@@ -59,6 +83,7 @@
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 92px;
|
||||
min-width: 0;
|
||||
max-height: calc(100vh - 112px);
|
||||
padding-right: 10px;
|
||||
overflow-y: auto;
|
||||
@@ -67,8 +92,10 @@
|
||||
|
||||
.sidebar a {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
text-decoration: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
@@ -77,6 +104,7 @@
|
||||
}
|
||||
|
||||
.layerDescription {
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
border-radius: var(--mantine-radius-xs);
|
||||
@@ -85,7 +113,9 @@
|
||||
|
||||
.layerDescription code {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.documentation {
|
||||
@@ -100,6 +130,7 @@
|
||||
|
||||
.schemaSection {
|
||||
scroll-margin-top: 96px;
|
||||
min-width: 0;
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
@@ -112,6 +143,7 @@
|
||||
|
||||
.definition {
|
||||
scroll-margin-top: 96px;
|
||||
min-width: 0;
|
||||
margin-top: 38px;
|
||||
padding-top: 32px;
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
@@ -126,6 +158,8 @@
|
||||
}
|
||||
|
||||
.yamlDetails {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@@ -139,6 +173,8 @@
|
||||
}
|
||||
|
||||
.yamlBlock {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px 12px 16px 18px;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
@@ -148,6 +184,9 @@
|
||||
|
||||
.yamlBlock code {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
@@ -162,6 +201,12 @@
|
||||
border-radius: var(--mantine-radius-xs);
|
||||
}
|
||||
|
||||
.tableScroll {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
|
||||
}
|
||||
@@ -179,7 +224,7 @@
|
||||
|
||||
@media (max-width: 62em) {
|
||||
.docsGrid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
||||
@@ -145,7 +145,7 @@ function FieldTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={620}>
|
||||
<Table.ScrollContainer minWidth={620} className={classes.tableScroll}>
|
||||
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
@@ -285,7 +285,7 @@ function EnumDefinition({ item, layer }: { item: ProtoEnum; layer: ProtoLayer })
|
||||
{item.description || `Enumeration ${item.name} in protocol layer ${layer.version}.`}
|
||||
</Text>
|
||||
<HiddenYaml value={item.yaml} />
|
||||
<Table.ScrollContainer minWidth={520}>
|
||||
<Table.ScrollContainer minWidth={520} className={classes.tableScroll}>
|
||||
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
@@ -517,6 +517,56 @@ export class RccGeneratedPacketRegistry {
|
||||
</Code>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection title="RCC: one schema contract, two generation modes">
|
||||
<LegalText>
|
||||
Rosetta Code Compiler (RCC) is the build-time compiler behind the protocol.
|
||||
It reads every <Code>layer.N.yml</Code> snapshot, turns the YAML into a
|
||||
typed protocol model, validates that the history is safe, and emits source
|
||||
code for the selected language. Applications import generated models and
|
||||
codecs; they do not parse the YAML at runtime.
|
||||
</LegalText>
|
||||
<Code block className={classes.protoCode}>
|
||||
{`proto/layer.*.yml
|
||||
│
|
||||
├─ parse packets, reusable types, enums and comments
|
||||
├─ validate references, required fields and stable field IDs
|
||||
│
|
||||
├─ mode=server → all layers + version-aware codecs + registry
|
||||
└─ mode=client → latest layer + latest codecs + version constant`}
|
||||
</Code>
|
||||
<LegalList>
|
||||
<List.Item>
|
||||
<strong>Server generation</strong> preserves the protocol timeline.
|
||||
Java codecs receive the negotiated layer, include only fields active in
|
||||
that layer, and expose the compiled minimum and maximum versions.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<strong>Client generation</strong> selects the{' '}
|
||||
<Text component="span" className={classes.important}>
|
||||
highest layer
|
||||
</Text>
|
||||
{' '}and creates the current packet, type, enum, codec, and registry
|
||||
sources without carrying historical shapes into the application.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<strong>Schema validation</strong> fails the build for unknown types or
|
||||
unsafe field-ID reuse, so incompatible protocol edits are caught before
|
||||
generated sources reach a server or client package.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<strong>Generated documentation</strong> carries schema comments into
|
||||
Javadoc or TSDoc, including field direction, wire type, optionality, and
|
||||
accessor contracts.
|
||||
</List.Item>
|
||||
</LegalList>
|
||||
<LegalText>
|
||||
The RCC guide includes separate client and version-aware server examples.{' '}
|
||||
Run <Code className={classes.important}>npx rcc --list-languages</Code> for
|
||||
the authoritative list of language targets in the installed compiler
|
||||
version.
|
||||
</LegalText>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection title="How an RCC YAML layer is structured">
|
||||
<LegalText>
|
||||
RCC reads versioned files named <Code>layer.N.yml</Code>. The top-level
|
||||
@@ -624,7 +674,7 @@ reader.getInt8(fieldId: 1) -> ResultCode.SUCCESS`}
|
||||
^^^^^ ^^^^^^^^
|
||||
packet field 1 · INT8 · SUCCESS (0)`}
|
||||
</Code>
|
||||
<Table.ScrollContainer minWidth={560}>
|
||||
<Table.ScrollContainer minWidth={560} className={classes.tableScroll}>
|
||||
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.comparisonTable}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
@@ -664,9 +714,14 @@ packet field 1 · INT8 · SUCCESS (0)`}
|
||||
</LegalText>
|
||||
</LegalSection>
|
||||
|
||||
<Group className={classes.docsLinks}>
|
||||
<Anchor component={Link} to="/proto/docs" className={classes.docsLink}>
|
||||
See docs of PROTO →
|
||||
</Anchor>
|
||||
<Anchor component={Link} to="/rcc" className={classes.docsLink}>
|
||||
See docs of RCC →
|
||||
</Anchor>
|
||||
</Group>
|
||||
</LegalPage>
|
||||
</>
|
||||
);
|
||||
|
||||
119
src/pages/RccPage.module.css
Normal file
119
src/pages/RccPage.module.css
Normal file
@@ -0,0 +1,119 @@
|
||||
.code {
|
||||
margin: 18px 0;
|
||||
padding: 16px;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
border-radius: var(--mantine-radius-xs);
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
font-size: 12px;
|
||||
line-height: 1.65;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.pipeline {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin: 22px 0;
|
||||
}
|
||||
|
||||
.pipeline > span:nth-child(odd) {
|
||||
padding: 14px;
|
||||
border: 1px solid light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-9));
|
||||
border-radius: var(--mantine-radius-xs);
|
||||
background: light-dark(var(--mantine-color-blue-0), rgba(25, 113, 194, 0.12));
|
||||
color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-1));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 650;
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pipeline > span:nth-child(even) {
|
||||
color: var(--mantine-color-blue-6);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.step:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.stepNumber {
|
||||
display: grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mantine-color-blue-6);
|
||||
color: var(--mantine-color-white);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stepBody {
|
||||
min-width: 0;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.step:last-child .stepBody {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
margin: 3px 0 12px;
|
||||
color: light-dark(var(--mantine-color-dark-8), var(--mantine-color-dark-0));
|
||||
font-size: 17px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.important {
|
||||
padding: 0 0.14em;
|
||||
border-radius: 2px;
|
||||
background: light-dark(rgba(250, 82, 82, 0.13), rgba(250, 82, 82, 0.2));
|
||||
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-2));
|
||||
font-weight: inherit;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 18px 0;
|
||||
border: 1px solid light-dark(var(--mantine-color-red-3), var(--mantine-color-red-9));
|
||||
border-left: 3px solid var(--mantine-color-red-6);
|
||||
background: light-dark(var(--mantine-color-red-0), rgba(250, 82, 82, 0.1));
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
.pipeline {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.pipeline > span:nth-child(even) {
|
||||
transform: rotate(90deg);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 36em) {
|
||||
.step {
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stepNumber {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
249
src/pages/RccPage.tsx
Normal file
249
src/pages/RccPage.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { Anchor, Box, Code, List, Paper, Text, Title } from '@mantine/core';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
LegalList,
|
||||
LegalPage,
|
||||
LegalSection,
|
||||
LegalText,
|
||||
} from '../components/LegalPage/LegalPage';
|
||||
import { SEO } from '../components/SEO/SEO';
|
||||
import classes from './RccPage.module.css';
|
||||
|
||||
const GITEA_REPOSITORY = 'https://git.rosetta.im/Rosetta/rcc';
|
||||
|
||||
function TechnicalCode({ children }: { children: string }) {
|
||||
return (
|
||||
<Code block className={classes.code}>
|
||||
{children}
|
||||
</Code>
|
||||
);
|
||||
}
|
||||
|
||||
function Step({ number, title, children }: { number: number; title: string; children: ReactNode }) {
|
||||
return (
|
||||
<Box className={classes.step}>
|
||||
<Text className={classes.stepNumber}>{number}</Text>
|
||||
<Box className={classes.stepBody}>
|
||||
<Title order={3} className={classes.subheading}>
|
||||
{title}
|
||||
</Title>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function RccPage() {
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title="Rosetta Code Compiler (RCC)"
|
||||
description="Install RCC and generate validated protocol models, codecs, and registries from versioned Rosetta YAML schemas."
|
||||
canonical="https://rosetta.im/rcc/"
|
||||
keywords="RCC, Rosetta Code Compiler, code generation, protocol codecs, YAML protocol"
|
||||
/>
|
||||
<LegalPage eyebrow="Developer tooling" title="Rosetta Code Compiler">
|
||||
<LegalText lead>
|
||||
RCC compiles Rosetta's versioned YAML protocol into typed models, codecs,
|
||||
and packet registries. A server build can retain the{' '}
|
||||
<Text component="span" className={classes.important}>
|
||||
complete protocol history
|
||||
</Text>
|
||||
; a client build stays small by generating only the{' '}
|
||||
<Text component="span" className={classes.important}>
|
||||
latest layer
|
||||
</Text>
|
||||
.
|
||||
</LegalText>
|
||||
|
||||
<LegalSection title="What RCC produces">
|
||||
<LegalText>
|
||||
RCC treats <Code>proto/layer.*.yml</Code> as the protocol's source of
|
||||
truth. Before writing code, it resolves primitive, enum, and reusable type
|
||||
references and rejects incompatible reuse of a field ID between layers.
|
||||
</LegalText>
|
||||
<Box className={classes.pipeline}>
|
||||
<Text component="span">Versioned YAML schemas</Text>
|
||||
<Text component="span" aria-hidden="true">→</Text>
|
||||
<Text component="span">Parse + validate</Text>
|
||||
<Text component="span" aria-hidden="true">→</Text>
|
||||
<Text component="span">Models + codecs + registry</Text>
|
||||
</Box>
|
||||
<LegalList>
|
||||
<List.Item>
|
||||
Packets become typed packet classes; reusable schema types become model
|
||||
classes; enums preserve their numeric wire values.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Codecs encode and decode TLV fields and verify required fields before
|
||||
encoding and after decoding.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
The generated registry maps packet IDs to codecs and exposes the
|
||||
compiled protocol version or version range.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
YAML comments become Javadoc or TSDoc for generated declarations and
|
||||
accessors.
|
||||
</List.Item>
|
||||
</LegalList>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection title="Install RCC from Rosetta Gitea">
|
||||
<LegalText>
|
||||
Add Rosetta's npm package registry to the project, then install RCC. The
|
||||
registry setting can live in a project-level <Code>.npmrc</Code> so CI and
|
||||
local builds resolve the same compiler package.
|
||||
</LegalText>
|
||||
<TechnicalCode>{`# .npmrc
|
||||
registry=https://git.rosetta.im/api/packages/Rosetta/npm/
|
||||
|
||||
# configure read-only registry authentication
|
||||
npm config set -- \\
|
||||
'//git.rosetta.im/api/packages/Rosetta/npm/:_authToken' \\
|
||||
'b5e64acbf1fdbe2d6d2bc1fac95333ef6f905fff'
|
||||
|
||||
# install the compiler
|
||||
npm install rcc
|
||||
|
||||
# query languages from the installed RCC version
|
||||
npx rcc --list-languages
|
||||
|
||||
# inspect the rest of the CLI
|
||||
npx rcc --help`}</TechnicalCode>
|
||||
<LegalText>
|
||||
RCC source is hosted in the{' '}
|
||||
<Anchor href={GITEA_REPOSITORY} target="_blank" rel="noreferrer">
|
||||
Rosetta/rcc Gitea repository
|
||||
</Anchor>
|
||||
.{' '}
|
||||
<Text component="span" className={classes.important}>
|
||||
Pin the dependency version
|
||||
</Text>
|
||||
{' '}in the project lockfile so generated output remains reproducible.
|
||||
</LegalText>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection title="Generate from schemas, step by step">
|
||||
<Step number={1} title="Create versioned schema layers">
|
||||
<LegalText>
|
||||
Put complete protocol snapshots in one directory and name them
|
||||
<Code> layer.N.yml</Code>. Increment the top-level version, keep packet
|
||||
and field IDs stable, and repeat definitions that remain part of the
|
||||
protocol.
|
||||
</LegalText>
|
||||
<TechnicalCode>{`proto/
|
||||
├── layer.1.yml
|
||||
├── layer.2.yml
|
||||
└── layer.3.yml`}</TechnicalCode>
|
||||
</Step>
|
||||
|
||||
<Step number={2} title="Choose client or server output">
|
||||
<LegalText>
|
||||
Use <Code>client</Code> for the current application package. RCC selects
|
||||
the highest schema version. Use <Code>server</Code> when the runtime must
|
||||
decode connections negotiated on older supported layers. Full
|
||||
multi-version codec generation is currently implemented by the Java
|
||||
backend.
|
||||
</LegalText>
|
||||
</Step>
|
||||
|
||||
<Step number={3} title="Generate Java sources">
|
||||
<LegalText>
|
||||
Server generation combines all layers and emits version-aware codecs,
|
||||
models, enums, and a registry with <Code>MIN_VERSION</Code> and
|
||||
<Code> MAX_VERSION</Code>.
|
||||
</LegalText>
|
||||
<TechnicalCode>{`npx rcc \\
|
||||
--mode server \\
|
||||
--lang java \\
|
||||
--protoDir ./proto \\
|
||||
--outDir ./server/src/generated/java \\
|
||||
--basePackage im.rosetta.network \\
|
||||
--clean`}</TechnicalCode>
|
||||
<LegalText>
|
||||
For a Java client, change the mode and output directory. The generated
|
||||
sources describe only the latest layer.
|
||||
</LegalText>
|
||||
<TechnicalCode>{`npx rcc \\
|
||||
--mode client \\
|
||||
--lang java \\
|
||||
--protoDir ./proto \\
|
||||
--outDir ./client/src/generated/java \\
|
||||
--basePackage im.rosetta.network \\
|
||||
--clean`}</TechnicalCode>
|
||||
</Step>
|
||||
|
||||
<Step number={4} title="Generate TypeScript sources">
|
||||
<LegalText>
|
||||
TypeScript is the current latest-layer client target. It produces
|
||||
models, enums, codecs, and <Code>RccGeneratedPacketRegistry.VERSION</Code>.
|
||||
</LegalText>
|
||||
<TechnicalCode>{`npx rcc \\
|
||||
--mode client \\
|
||||
--lang typescript \\
|
||||
--protoDir ./proto \\
|
||||
--outDir ./client/src/generated/protocol \\
|
||||
--clean`}</TechnicalCode>
|
||||
<Paper className={classes.note} radius="sm" p="md">
|
||||
<Text size="sm" lh={1.7}>
|
||||
Always run{' '}
|
||||
<Code className={classes.important}>npx rcc --list-languages</Code>{' '}
|
||||
before configuring a generator. The installed compiler's output is
|
||||
the source of truth for available language targets.
|
||||
</Text>
|
||||
</Paper>
|
||||
</Step>
|
||||
|
||||
<Step number={5} title="Run all targets from one prebuild config">
|
||||
<LegalText>
|
||||
Keep client and server generation together in
|
||||
<Code> rcc-prebuild.yml</Code>. Paths are resolved relative to the
|
||||
config file; <Code>clean: true</Code> clears only each configured output
|
||||
directory.
|
||||
</LegalText>
|
||||
<TechnicalCode>{`watch: false
|
||||
protoDir: ./proto
|
||||
clean: true
|
||||
|
||||
compilers:
|
||||
- name: TypeScript client
|
||||
mode: client
|
||||
lang: typescript
|
||||
output: ./client/src/generated/protocol
|
||||
|
||||
- name: Java server
|
||||
mode: server
|
||||
lang: java
|
||||
output: ./server/src/generated/java
|
||||
basePackage: im.rosetta.network`}</TechnicalCode>
|
||||
<TechnicalCode>{`npx rcc -f ./rcc-prebuild.yml`}</TechnicalCode>
|
||||
</Step>
|
||||
|
||||
<Step number={6} title="Compile and package the generated sources">
|
||||
<LegalText>
|
||||
Add the output directory to the language build, then run generation
|
||||
before compilation in local development and CI. RCC writes source
|
||||
files; the surrounding Gradle, npm, or application build is responsible
|
||||
for compiling and publishing the final server or client package.
|
||||
</LegalText>
|
||||
</Step>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection title="CLI reference">
|
||||
<TechnicalCode>{`npx rcc --mode <client|server> [options]
|
||||
npx rcc -f <filename.yml>
|
||||
npx rcc --list-languages
|
||||
|
||||
--mode, -m client | server
|
||||
--lang, -l language returned by --list-languages
|
||||
--protoDir, -p schema directory (default: ./proto)
|
||||
--outDir, -o generated source directory (default: ./generated)
|
||||
--basePackage, -b Java base package (default: im.rosetta.generated)
|
||||
--clean clean the output directory before generation
|
||||
--file, -f prebuild YAML config`}</TechnicalCode>
|
||||
</LegalSection>
|
||||
</LegalPage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -218,6 +218,60 @@ export function TermsPage() {
|
||||
Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor>
|
||||
</LegalText>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection title="20. Account Deletion" anchorId="account-deletion">
|
||||
<LegalText>
|
||||
You can permanently delete your Rosetta account in the app. This action
|
||||
cannot be undone. Rosetta accounts use cryptographic identities, so no phone
|
||||
number or email address is required to create or delete one.
|
||||
</LegalText>
|
||||
<LegalText>To delete the account currently signed in on an Android device:</LegalText>
|
||||
<LegalList>
|
||||
<List.Item>Open Rosetta and sign in to the account you want to remove.</List.Item>
|
||||
<List.Item>Open Settings, then choose Safety.</List.Item>
|
||||
<List.Item>Select Delete Account.</List.Item>
|
||||
<List.Item>Review the confirmation and select Delete.</List.Item>
|
||||
</LegalList>
|
||||
<LegalText>
|
||||
Deleting an account removes its local account record, message history,
|
||||
conversations, cached profile media, and other account-scoped data from the
|
||||
device where you complete the deletion. It also ends the signed-in session
|
||||
for that account on that device.
|
||||
</LegalText>
|
||||
<LegalText>
|
||||
Rosetta is designed so readable message history stays on users' devices,
|
||||
not as a readable server-side message archive. Encrypted data in transit is
|
||||
processed only as needed to deliver the service. Deletion does not remove
|
||||
messages, attachments, or other data that other people have already received
|
||||
and stored on their own devices.
|
||||
</LegalText>
|
||||
<LegalText>
|
||||
Limited technical logs and support communications may be retained only for
|
||||
as long as reasonably necessary for security, reliability, support, legal,
|
||||
and operational purposes, as described in our{' '}
|
||||
<Anchor component={Link} to="/privacy">
|
||||
Privacy Policy
|
||||
</Anchor>
|
||||
.
|
||||
</LegalText>
|
||||
<LegalText>
|
||||
If you cannot sign in or need help with an account-deletion request, email{' '}
|
||||
<Anchor href="mailto:admin@rosetta.im?subject=Rosetta%20Account%20Deletion%20Request">
|
||||
admin@rosetta.im
|
||||
</Anchor>
|
||||
. Include the public account identifier, if available, and enough detail for
|
||||
us to locate the request. We may ask you to verify control of the account or
|
||||
its keys before acting on a request. Never send your private key or recovery
|
||||
phrase by email or to support.
|
||||
</LegalText>
|
||||
<LegalText>
|
||||
For help, contact{' '}
|
||||
<Anchor href="mailto:support@rosetta.im?subject=Rosetta%20Account%20Deletion%20Help">
|
||||
support@rosetta.im
|
||||
</Anchor>
|
||||
.
|
||||
</LegalText>
|
||||
</LegalSection>
|
||||
</LegalPage>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
function withFreshHtml(response) {
|
||||
if (!response.headers.get('content-type')?.includes('text/html')) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set('Cache-Control', 'no-cache');
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
const worker = {
|
||||
async fetch(request, env) {
|
||||
const response = await env.ASSETS.fetch(request);
|
||||
@@ -7,11 +22,12 @@ const worker = {
|
||||
request.method !== 'GET' ||
|
||||
!request.headers.get('accept')?.includes('text/html')
|
||||
) {
|
||||
return response;
|
||||
return withFreshHtml(response);
|
||||
}
|
||||
|
||||
const indexUrl = new URL('/index.html', request.url);
|
||||
return env.ASSETS.fetch(new Request(indexUrl, request));
|
||||
const indexResponse = await env.ASSETS.fetch(new Request(indexUrl, request));
|
||||
return withFreshHtml(indexResponse);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user