forked from Rosetta/landing
Redesign landing and generate Proto docs
This commit is contained in:
77
src/App.tsx
77
src/App.tsx
@@ -1,42 +1,57 @@
|
||||
import '@mantine/core/styles.css';
|
||||
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { Center, createTheme, Loader, MantineProvider } from '@mantine/core';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { HeroTitle } from './components/HeroTitle/HeroTitle';
|
||||
import { DownloadCenter } from './components/DownloadCenter/DownloadCenter';
|
||||
import { FeaturesGrid } from './components/FeaturesGrid/FeaturesGrid';
|
||||
import { SEO } from './components/SEO/SEO';
|
||||
import './style.css'
|
||||
import { MessageSteps } from './components/MessageSteps/MessageSteps';
|
||||
import { Footer } from './components/Footer/Footer';
|
||||
import { PrivacyPage } from './pages/PrivacyPage';
|
||||
import { SupportPage } from './pages/SupportPage';
|
||||
import { TermsPage } from './pages/TermsPage';
|
||||
import { ScrollToTop } from './components/ScrollToTop/ScrollToTop';
|
||||
import './style.css';
|
||||
|
||||
function LandingPage() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}>
|
||||
© {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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
© {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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
16
src/components/SiteFooter/SiteFooter.module.css
Normal file
16
src/components/SiteFooter/SiteFooter.module.css
Normal 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));
|
||||
}
|
||||
66
src/components/SiteFooter/SiteFooter.tsx
Normal file
66
src/components/SiteFooter/SiteFooter.tsx
Normal 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}>
|
||||
© {year} ROSETTA CLOUD SERVICES LIMITED
|
||||
</Text>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
58
src/components/SiteHeader/SiteHeader.module.css
Normal file
58
src/components/SiteHeader/SiteHeader.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
80
src/components/SiteHeader/SiteHeader.tsx
Normal file
80
src/components/SiteHeader/SiteHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
3707
src/generated/protoDocs.ts
Normal file
File diff suppressed because it is too large
Load Diff
241
src/pages/LandingPage.module.css
Normal file
241
src/pages/LandingPage.module.css
Normal 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
422
src/pages/LandingPage.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 ("Rosetta",
|
||||
"we", "us", or "our"). This Privacy Policy
|
||||
|
||||
159
src/pages/ProtoPage.module.css
Normal file
159
src/pages/ProtoPage.module.css
Normal 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
483
src/pages/ProtoPage.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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."
|
||||
>
|
||||
|
||||
@@ -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 ("Terms") govern
|
||||
your use of Rosetta, an end-to-end encrypted messaging application operated by
|
||||
|
||||
42
src/services/updates.ts
Normal file
42
src/services/updates.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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
42
src/types/proto.ts
Normal 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[];
|
||||
}
|
||||
Reference in New Issue
Block a user