Move support into React Router

This commit is contained in:
rosetta
2026-07-30 16:47:54 -07:00
parent c81c3a1189
commit 650d73830f
11 changed files with 378 additions and 838 deletions

View File

@@ -12,14 +12,11 @@
<meta property="og:url" content="https://rosetta.im/" /> <meta property="og:url" content="https://rosetta.im/" />
<meta property="og:title" content="Rosetta - Secure Messaging for Everyone" /> <meta property="og:title" content="Rosetta - Secure Messaging for Everyone" />
<meta property="og:description" content="End-to-end encrypted messaging app with zero-knowledge architecture. Download for Windows, Linux, and Android." /> <meta property="og:description" content="End-to-end encrypted messaging app with zero-knowledge architecture. Download for Windows, Linux, and Android." />
<meta property="og:image" content="/og.png" />
<meta property="og:site_name" content="Rosetta" /> <meta property="og:site_name" content="Rosetta" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://rosetta.im/" /> <meta name="twitter:url" content="https://rosetta.im/" />
<meta name="twitter:title" content="Rosetta - Secure Messaging for Everyone" /> <meta name="twitter:title" content="Rosetta - Secure Messaging for Everyone" />
<meta name="twitter:description" content="End-to-end encrypted messaging app with zero-knowledge architecture." /> <meta name="twitter:description" content="End-to-end encrypted messaging app with zero-knowledge architecture." />
<meta name="twitter:image" content="/og.png" />
<link rel="canonical" href="https://rosetta.im/" /> <link rel="canonical" href="https://rosetta.im/" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
@@ -44,8 +41,7 @@
"@type": "AggregateRating", "@type": "AggregateRating",
"ratingValue": "4.8", "ratingValue": "4.8",
"ratingCount": "1500" "ratingCount": "1500"
}, }
"image": "/og.png"
} }
</script> </script>
</head> </head>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -1,778 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rosetta Support</title>
<meta
name="description"
content="Contact Rosetta Support for help with account access, recovery phrases, messaging, media, privacy, and security."
/>
<link rel="canonical" href="https://rosetta.im/support/" />
<meta name="robots" content="index, follow" />
<meta name="theme-color" content="#0a67ff" />
<script>
document.documentElement.classList.add("js");
</script>
<style>
:root {
color-scheme: light dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #f6f8fc;
line-height: 1.5;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background: #f6f8fc;
}
a {
text-decoration: none;
}
a {
color: #075ed3;
}
a:focus-visible {
outline: 3px solid #8dc0ff;
outline-offset: 3px;
}
.page {
width: min(1120px, calc(100% - 40px));
margin: 0 auto;
padding: 24px 0 64px;
}
.js .page > * {
opacity: 0;
transform: translateY(14px);
transition: opacity 420ms ease, transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
}
.js .page.is-ready > * {
opacity: 1;
transform: translateY(0);
}
.js .page.is-ready > :nth-child(1) {
transition-delay: 0ms;
}
.js .page.is-ready > :nth-child(2) {
transition-delay: 45ms;
}
.js .page.is-ready > :nth-child(3) {
transition-delay: 90ms;
}
.js .page.is-ready > :nth-child(n + 4) {
transition-delay: 130ms;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
color: inherit;
font-size: 18px;
font-weight: 760;
letter-spacing: 0;
text-decoration: none;
}
.brand-mark {
width: 34px;
height: 34px;
border-radius: 8px;
overflow: hidden;
}
.brand-mark img {
display: block;
width: 100%;
height: 100%;
}
.home-link {
color: #334155;
font-size: 15px;
font-weight: 600;
text-decoration: none;
}
.hero {
max-width: 760px;
padding: 88px 0 58px;
}
.eyebrow {
margin: 0 0 12px;
color: #075ed3;
font-size: 14px;
font-weight: 750;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
max-width: 650px;
margin-bottom: 20px;
font-size: clamp(38px, 7vw, 68px);
font-weight: 760;
letter-spacing: 0;
line-height: 1.04;
}
.intro {
max-width: 620px;
margin-bottom: 28px;
color: #4b5568;
font-size: clamp(18px, 2.2vw, 21px);
line-height: 1.5;
}
.contact-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 50px;
padding: 12px 18px;
border-radius: 8px;
background: #0a67ff;
color: #ffffff;
font-size: 16px;
font-weight: 720;
text-decoration: none;
}
.contact-button:hover {
background: #0055ca;
}
.quick-links {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 72px;
}
.quick-link {
display: block;
min-height: 138px;
padding: 22px;
border: 1px solid #dbe2ee;
border-radius: 8px;
background: #ffffff;
color: inherit;
text-decoration: none;
}
.quick-link:hover {
border-color: #a7c9ff;
box-shadow: 0 10px 24px rgba(26, 61, 110, 0.08);
}
.quick-link strong {
display: block;
margin-bottom: 8px;
font-size: 17px;
}
.quick-link span {
color: #64748b;
font-size: 15px;
line-height: 1.4;
}
.section {
padding: 52px 0;
border-top: 1px solid #dbe2ee;
}
.section-heading {
display: grid;
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
gap: 32px;
align-items: start;
margin-bottom: 28px;
}
h2 {
margin-bottom: 0;
font-size: clamp(28px, 4vw, 38px);
letter-spacing: 0;
line-height: 1.12;
}
.section-copy {
margin-bottom: 0;
color: #526176;
font-size: 17px;
}
.faq {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
details {
display: block;
box-sizing: border-box;
padding: 19px 20px;
border: 1px solid #dbe2ee;
border-radius: 8px;
background: #ffffff;
overflow: hidden;
transition: border-color 180ms ease, background-color 180ms ease;
}
details[open] {
border-color: #b8cce8;
background: #f9fbff;
}
details.is-animating {
will-change: height;
}
summary {
display: grid;
grid-template-columns: minmax(0, 1fr) 36px;
align-items: center;
gap: 16px;
min-height: 28px;
cursor: pointer;
color: #1d293d;
font-size: 17px;
font-weight: 710;
list-style: none;
}
summary::-webkit-details-marker {
display: none;
}
summary::marker {
content: "";
}
.faq-toggle {
display: grid;
width: 36px;
height: 36px;
place-items: center;
justify-self: end;
border: 1px solid #c3d5ed;
border-radius: 50%;
background: #f3f8ff;
color: #075ed3;
transition: background-color 180ms ease, border-color 180ms ease;
}
.faq-toggle svg {
display: block;
width: 18px;
height: 18px;
stroke: currentColor;
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
transform: translateZ(0) rotate(0deg);
will-change: transform;
transition: transform 240ms cubic-bezier(0.25, 0.1, 0.25, 1);
}
details[open] .faq-toggle {
border-color: #78a8df;
background: #deedff;
}
details[open] .faq-toggle svg {
transform: translateZ(0) rotate(180deg);
}
summary:focus-visible {
outline: none;
}
summary:focus-visible .faq-toggle {
box-shadow: 0 0 0 3px #8cbeff;
}
details > p {
display: block;
margin: 0 40px 0 0;
color: #526176;
font-size: 15px;
opacity: 0;
transform: translateY(-3px);
transition: opacity 160ms ease, transform 220ms cubic-bezier(0.25, 0.1, 0.25, 1);
}
details:not([open]) > p {
display: none;
}
details[open] > p {
margin: 14px 40px 0 0;
opacity: 1;
transform: translateY(0);
}
details.is-closing > p {
margin: 0 40px 0 0;
opacity: 0;
transform: translateY(-3px);
}
@media (prefers-reduced-motion: reduce) {
.js .page > * {
opacity: 1;
transform: none;
transition: none;
}
details,
.faq-toggle,
.faq-toggle svg,
details > p {
transition: none;
}
}
.contact-panel {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(240px, 0.9fr);
gap: 28px;
padding: 32px;
border-radius: 8px;
background: #0e1e39;
color: #ffffff;
}
.contact-panel h2 {
margin-bottom: 12px;
}
.contact-panel p {
max-width: 580px;
margin-bottom: 0;
color: #cbd7ed;
font-size: 17px;
}
.contact-details {
align-self: center;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 8px;
background: rgba(255, 255, 255, 0.07);
}
.contact-details strong,
.contact-details a {
display: block;
}
.contact-details strong {
margin-bottom: 6px;
font-size: 15px;
}
.contact-details a {
color: #ffffff;
font-size: 18px;
font-weight: 700;
overflow-wrap: anywhere;
text-decoration: none;
}
.legal-links {
display: flex;
flex-wrap: wrap;
gap: 18px;
padding-top: 32px;
}
.legal-links a {
color: #526176;
font-size: 15px;
text-decoration: none;
}
@media (max-width: 720px) {
.page {
width: min(100% - 32px, 1120px);
padding-top: 16px;
}
.hero {
padding: 64px 0 42px;
}
.quick-links,
.faq,
.section-heading,
.contact-panel {
grid-template-columns: 1fr;
}
.quick-links {
margin-bottom: 52px;
}
.section {
padding: 40px 0;
}
.contact-panel {
gap: 20px;
padding: 24px;
}
}
@media (prefers-color-scheme: dark) {
:root {
color: #eff4ff;
background: #0c111a;
}
body {
background: #0c111a;
}
a,
.eyebrow {
color: #72afff;
}
.home-link,
.quick-link,
summary {
color: #eef4ff;
}
.faq-toggle {
color: #ffffff;
border-color: #354c6d;
background: #1d2b3f;
}
details[open] .faq-toggle {
border-color: #5e92ca;
background: #29466c;
}
.intro,
.section-copy,
.quick-link span,
details p,
.legal-links a {
color: #aab8cf;
}
.quick-link,
details {
border-color: #29354a;
background: #141c29;
}
details[open] {
border-color: #3c608b;
background: #18263a;
}
.quick-link:hover {
border-color: #427fc7;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
}
.section {
border-color: #29354a;
}
.contact-button {
background: #3986ff;
}
.contact-button:hover {
background: #5b9bff;
}
}
</style>
</head>
<body>
<main class="page">
<header class="topbar">
<a class="brand" href="/" aria-label="Rosetta home">
<span class="brand-mark"><img src="/rosetta-logo.png" alt="" /></span>
Rosetta
</a>
<a class="home-link" href="/">Back to home</a>
</header>
<section class="hero" aria-labelledby="support-title">
<p class="eyebrow">Rosetta Support</p>
<h1 id="support-title">Help with your Rosetta account and messages.</h1>
<p class="intro">
Find answers about account access, recovery phrases, private messaging,
shared media, and privacy. For anything else, contact our support team.
</p>
<a class="contact-button" href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request">
Email Rosetta Support
</a>
</section>
<nav class="quick-links" aria-label="Support topics">
<a class="quick-link" href="#account">
<strong>Account and access</strong>
<span>Recovery phrases, device access, and account security.</span>
</a>
<a class="quick-link" href="#messaging">
<strong>Messaging and media</strong>
<span>Messages, photos, videos, files, voice messages, and groups.</span>
</a>
<a class="quick-link" href="#privacy">
<strong>Privacy and safety</strong>
<span>Security questions, privacy information, and abuse reports.</span>
</a>
</nav>
<section class="section" id="account" aria-labelledby="account-heading">
<div class="section-heading">
<h2 id="account-heading">Account and access</h2>
<p class="section-copy">
Rosetta accounts are accessed with a recovery phrase instead of a phone number.
Keep your phrase private and stored somewhere safe.
</p>
</div>
<div class="faq">
<details>
<summary><span>How do I access my account on a new device?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
Install Rosetta and enter your recovery phrase in the exact order it was created.
Rosetta may ask you to confirm a new device to protect your account.
</p>
</details>
<details>
<summary><span>I lost my recovery phrase. Can it be restored?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
No. Your recovery phrase is the key to your account. Rosetta cannot recover it
for you, so keep it private and backed up before you need it.
</p>
</details>
<details>
<summary><span>Why am I asked to confirm a new device?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
New-device confirmation helps prevent someone who has obtained a recovery phrase
from silently accessing your account on another device.
</p>
</details>
<details>
<summary><span>How do I report an account-access problem?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
Email us with the device model, Rosetta version, and a short description of what
happened. Do not include your recovery phrase or private keys.
</p>
</details>
</div>
</section>
<section class="section" id="messaging" aria-labelledby="messaging-heading">
<div class="section-heading">
<h2 id="messaging-heading">Messaging and media</h2>
<p class="section-copy">
Rosetta supports private chats, groups, photos, videos, files, and voice messages.
Attachment choices can vary depending on your device permissions and the selected item.
</p>
</div>
<div class="faq">
<details>
<summary><span>How do I send a photo, video, or file?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
Open a chat, tap the attachment button next to the message field, choose the item
you want to share, then send it. Allow Photos or Files access when iOS asks for it.
</p>
</details>
<details>
<summary><span>Why is an attachment still sending?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
Media uploads depend on the file size and your connection. Keep Rosetta open and
connected until the send indicator completes. You can retry a message that failed.
</p>
</details>
<details>
<summary><span>How do I send a voice message?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
In a chat, press and hold the microphone control to record, then release it to send.
Microphone permission is required the first time you record.
</p>
</details>
<details>
<summary><span>How do I get help with a group?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
Contact us with the group name and a description of the issue. Do not send private
recovery phrases, message contents, or encryption keys.
</p>
</details>
</div>
</section>
<section class="section" id="privacy" aria-labelledby="privacy-heading">
<div class="section-heading">
<h2 id="privacy-heading">Privacy and safety</h2>
<p class="section-copy">
Rosetta is designed for private communication. If you have a security concern or a
question about data handling, contact us directly.
</p>
</div>
<div class="faq">
<details>
<summary><span>How is message content protected?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
Rosetta is designed to encrypt message content before it is sent. For details about
how we handle data, read the Rosetta Privacy Policy.
</p>
</details>
<details>
<summary><span>How do I report abuse or a security issue?</span><span class="faq-toggle" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m6 9 6 6 6-6" /></svg></span></summary>
<p>
Email <a href="mailto:admin@rosetta.im?subject=Rosetta%20Security%20Report">admin@rosetta.im</a>
with a description of the concern. Do not include recovery phrases or private keys.
</p>
</details>
</div>
</section>
<section class="contact-panel" aria-labelledby="contact-heading">
<div>
<h2 id="contact-heading">Need more help?</h2>
<p>
Send us a message with your device model, app version, and a short explanation of the
issue. Never send a recovery phrase, private key, or private message content.
</p>
</div>
<div class="contact-details">
<strong>Rosetta Support</strong>
<a href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request">admin@rosetta.im</a>
</div>
</section>
<footer class="legal-links">
<a href="/">Rosetta home</a>
<a href="/privacy/">Privacy Policy</a>
<a href="/terms/">Terms of Use</a>
</footer>
</main>
<script>
window.addEventListener("DOMContentLoaded", () => {
requestAnimationFrame(() => {
document.querySelector(".page")?.classList.add("is-ready");
});
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const animationOptions = {
duration: 240,
easing: "cubic-bezier(0.25, 0.1, 0.25, 1)",
fill: "forwards",
};
document.querySelectorAll(".faq details").forEach((details) => {
const summary = details.querySelector("summary");
if (!summary) return;
let animation = null;
const setExpanded = (expanded) => summary.setAttribute("aria-expanded", String(expanded));
setExpanded(details.open);
summary.addEventListener("click", (event) => {
event.preventDefault();
if (animation) return;
if (prefersReducedMotion) {
details.open = !details.open;
setExpanded(details.open);
return;
}
const isOpening = !details.open;
const startHeight = details.getBoundingClientRect().height;
if (isOpening) {
details.open = true;
setExpanded(true);
const endHeight = details.scrollHeight;
details.classList.add("is-animating");
details.style.height = `${startHeight}px`;
animation = details.animate(
[{ height: `${startHeight}px` }, { height: `${endHeight}px` }],
animationOptions,
);
} else {
details.open = false;
const endHeight = details.getBoundingClientRect().height;
details.open = true;
details.classList.add("is-animating", "is-closing");
details.style.height = `${startHeight}px`;
animation = details.animate(
[{ height: `${startHeight}px` }, { height: `${endHeight}px` }],
animationOptions,
);
}
animation.onfinish = () => {
const completedAnimation = animation;
if (!isOpening) {
details.open = false;
setExpanded(false);
}
details.style.height = "";
details.classList.remove("is-animating", "is-closing");
completedAnimation.oncancel = null;
completedAnimation.cancel();
animation = null;
};
animation.oncancel = () => {
details.style.height = "";
details.classList.remove("is-animating", "is-closing");
animation = null;
};
});
});
});
</script>
</body>
</html>

View File

@@ -10,6 +10,7 @@ import './style.css'
import { MessageSteps } from './components/MessageSteps/MessageSteps'; import { MessageSteps } from './components/MessageSteps/MessageSteps';
import { Footer } from './components/Footer/Footer'; import { Footer } from './components/Footer/Footer';
import { PrivacyPage } from './pages/PrivacyPage'; import { PrivacyPage } from './pages/PrivacyPage';
import { SupportPage } from './pages/SupportPage';
import { TermsPage } from './pages/TermsPage'; import { TermsPage } from './pages/TermsPage';
import { ScrollToTop } from './components/ScrollToTop/ScrollToTop'; import { ScrollToTop } from './components/ScrollToTop/ScrollToTop';
@@ -32,6 +33,7 @@ export default function App() {
<ScrollToTop /> <ScrollToTop />
<Routes> <Routes>
<Route path="/" element={<LandingPage />} /> <Route path="/" element={<LandingPage />} />
<Route path="/support" element={<SupportPage />} />
<Route path="/privacy" element={<PrivacyPage />} /> <Route path="/privacy" element={<PrivacyPage />} />
<Route path="/terms" element={<TermsPage />} /> <Route path="/terms" element={<TermsPage />} />
</Routes> </Routes>

View File

@@ -13,7 +13,7 @@ export function Footer() {
&copy; {currentYear} {LEGAL_NAME}. All rights reserved. &copy; {currentYear} {LEGAL_NAME}. All rights reserved.
</Text> </Text>
<nav className={classes.links} aria-label="Legal information"> <nav className={classes.links} aria-label="Legal information">
<Anchor href="/support/" size="sm" c="dimmed"> <Anchor component={Link} to="/support" size="sm" c="dimmed">
Support Support
</Anchor> </Anchor>
<Anchor component={Link} to="/privacy" size="sm" c="dimmed"> <Anchor component={Link} to="/privacy" size="sm" c="dimmed">

View File

@@ -13,12 +13,13 @@ import { Link } from 'react-router-dom';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo'; import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import classes from './LegalPage.module.css'; import classes from './LegalPage.module.css';
type LegalRoute = 'privacy' | 'terms'; type ContentRoute = 'support' | 'privacy' | 'terms';
interface LegalPageProps { interface LegalPageProps {
active: LegalRoute; active: ContentRoute;
title: string; title: string;
updated: string; updated?: string;
eyebrow?: string;
children: ReactNode; children: ReactNode;
} }
@@ -27,7 +28,19 @@ interface LegalSectionProps {
children: ReactNode; children: ReactNode;
} }
export function LegalPage({ active, title, updated, children }: LegalPageProps) { 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(); const currentYear = new Date().getFullYear();
return ( return (
@@ -38,7 +51,7 @@ export function LegalPage({ active, title, updated, children }: LegalPageProps)
component={Link} component={Link}
to="/" to="/"
className={classes.brand} className={classes.brand}
aria-label="Rosetta home" aria-label="Rosetta"
> >
<RosettaLogo size={34} /> <RosettaLogo size={34} />
<Text component="span" fw={700} fz="lg"> <Text component="span" fw={700} fz="lg">
@@ -46,44 +59,36 @@ export function LegalPage({ active, title, updated, children }: LegalPageProps)
</Text> </Text>
</Anchor> </Anchor>
<Group component="nav" gap="lg" aria-label="Legal pages"> <Group component="nav" gap="lg" aria-label="Rosetta pages">
<Anchor href="/support/" c="dimmed" size="sm"> {NAV_LINKS.map(({ route, label }) => (
Support <Anchor
</Anchor> key={route}
<Anchor component={Link}
component={Link} to={`/${route}`}
to="/privacy" c={active === route ? 'blue' : 'dimmed'}
c={active === 'privacy' ? 'blue' : 'dimmed'} fw={active === route ? 600 : 400}
fw={active === 'privacy' ? 600 : 400} size="sm"
size="sm" aria-current={active === route ? 'page' : undefined}
aria-current={active === 'privacy' ? 'page' : undefined} >
> {label}
Privacy </Anchor>
</Anchor> ))}
<Anchor
component={Link}
to="/terms"
c={active === 'terms' ? 'blue' : 'dimmed'}
fw={active === 'terms' ? 600 : 400}
size="sm"
aria-current={active === 'terms' ? 'page' : undefined}
>
Terms
</Anchor>
</Group> </Group>
</Group> </Group>
<Box component="main" className={classes.main}> <Box component="main" className={classes.main}>
<Box className={classes.intro}> <Box className={classes.intro}>
<Text tt="uppercase" fw={700} fz="xs" c="blue" lts="0.08em"> <Text tt="uppercase" fw={700} fz="xs" c="blue" lts="0.08em">
Rosetta legal {eyebrow}
</Text> </Text>
<Title order={1} className={classes.title}> <Title order={1} className={classes.title}>
{title} {title}
</Title> </Title>
<Text c="dimmed" mt="md"> {updated && (
Last updated: {updated} <Text c="dimmed" mt="md">
</Text> Last updated: {updated}
</Text>
)}
</Box> </Box>
<Paper <Paper
@@ -103,18 +108,16 @@ export function LegalPage({ active, title, updated, children }: LegalPageProps)
</Text> </Text>
<Group component="nav" gap="md" aria-label="Footer links"> <Group component="nav" gap="md" aria-label="Footer links">
<Anchor component={Link} to="/" c="dimmed" size="xs"> <Anchor component={Link} to="/" c="dimmed" size="xs">
Rosetta home Rosetta
</Anchor> </Anchor>
<Anchor href="/support/" c="dimmed" size="xs"> <Anchor component={Link} to="/support" c="dimmed" size="xs">
Support Support
</Anchor> </Anchor>
<Anchor <Anchor component={Link} to="/privacy" c="dimmed" size="xs">
component={Link} Privacy Policy
to={active === 'privacy' ? '/terms' : '/privacy'} </Anchor>
c="dimmed" <Anchor component={Link} to="/terms" c="dimmed" size="xs">
size="xs" Terms of Use
>
{active === 'privacy' ? 'Terms of Use' : 'Privacy Policy'}
</Anchor> </Anchor>
</Group> </Group>
</Group> </Group>

View File

@@ -3,7 +3,6 @@ import { useEffect } from 'react';
interface SEOProps { interface SEOProps {
title?: string; title?: string;
description?: string; description?: string;
ogImage?: string;
keywords?: string; keywords?: string;
canonical?: string; canonical?: string;
} }
@@ -11,7 +10,6 @@ interface SEOProps {
export function SEO({ export function SEO({
title = 'Rosetta - Secure Messaging for Everyone', title = 'Rosetta - Secure Messaging for Everyone',
description = 'End-to-end encrypted messaging app with military-grade security. Download for Windows, Linux, and Android.', description = 'End-to-end encrypted messaging app with military-grade security. Download for Windows, Linux, and Android.',
ogImage = '/og.png',
keywords = 'secure messaging, encrypted chat, private messenger', keywords = 'secure messaging, encrypted chat, private messenger',
canonical = 'https://rosetta.im/' canonical = 'https://rosetta.im/'
}: SEOProps) { }: SEOProps) {
@@ -22,14 +20,14 @@ export function SEO({
updateMetaTag('name', 'keywords', keywords); updateMetaTag('name', 'keywords', keywords);
updateMetaTag('property', 'og:title', title); updateMetaTag('property', 'og:title', title);
updateMetaTag('property', 'og:description', description); updateMetaTag('property', 'og:description', description);
updateMetaTag('property', 'og:image', ogImage);
updateMetaTag('property', 'og:url', canonical); updateMetaTag('property', 'og:url', canonical);
updateMetaTag('name', 'twitter:title', title); updateMetaTag('name', 'twitter:title', title);
updateMetaTag('name', 'twitter:description', description); updateMetaTag('name', 'twitter:description', description);
updateMetaTag('name', 'twitter:image', ogImage);
updateMetaTag('name', 'twitter:url', canonical); updateMetaTag('name', 'twitter:url', canonical);
removeMetaTag('property', 'og:image');
removeMetaTag('name', 'twitter:image');
updateCanonical(canonical); updateCanonical(canonical);
}, [title, description, ogImage, keywords, canonical]); }, [title, description, keywords, canonical]);
return null; return null;
} }
@@ -46,6 +44,10 @@ function updateMetaTag(attr: string, name: string, content: string) {
element.setAttribute('content', content); element.setAttribute('content', content);
} }
function removeMetaTag(attr: string, name: string) {
document.querySelector(`meta[${attr}="${name}"]`)?.remove();
}
function updateCanonical(href: string) { function updateCanonical(href: string) {
let element = document.querySelector<HTMLLinkElement>('link[rel="canonical"]'); let element = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');

View File

@@ -1,4 +1,5 @@
import { Anchor, List } from '@mantine/core'; import { Anchor, List } from '@mantine/core';
import { Link } from 'react-router-dom';
import { import {
LegalAddress, LegalAddress,
LegalList, LegalList,
@@ -118,7 +119,7 @@ export function PrivacyPage() {
Depending on where you live, you may have rights to access, correct, delete, Depending on where you live, you may have rights to access, correct, delete,
or object to certain processing of your personal information. To make a or object to certain processing of your personal information. To make a
request, contact us at{' '} request, contact us at{' '}
<Anchor href="mailto:admin@rosetta.im">admin@rosetta.im</Anchor>. <Anchor href="mailto:admin@rosetta.im">Admin@rosetta.im</Anchor>.
</LegalText> </LegalText>
<LegalText> <LegalText>
Because Rosetta minimizes account data and uses cryptographic identifiers, Because Rosetta minimizes account data and uses cryptographic identifiers,
@@ -165,9 +166,12 @@ export function PrivacyPage() {
{'ROSETTA CLOUD SERVICES LIMITED\nRegistered address:\nUNIT 704C 7/F BLK 3 NAN FUNG IND CITY\n18 TIN HAU RD\nTUEN MUN\nHONG KONG'} {'ROSETTA CLOUD SERVICES LIMITED\nRegistered address:\nUNIT 704C 7/F BLK 3 NAN FUNG IND CITY\n18 TIN HAU RD\nTUEN MUN\nHONG KONG'}
</LegalAddress> </LegalAddress>
<LegalText> <LegalText>
Email: <Anchor href="mailto:admin@rosetta.im">admin@rosetta.im</Anchor> Email: <Anchor href="mailto:admin@rosetta.im">Admin@rosetta.im</Anchor>
<br /> <br />
Support: <Anchor href="/support/">https://rosetta.im/support/</Anchor> Support:{' '}
<Anchor component={Link} to="/support">
Rosetta Support
</Anchor>
</LegalText> </LegalText>
<LegalText> <LegalText>
Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor> Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor>

View File

@@ -0,0 +1,58 @@
.lead {
max-width: 680px;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
font-size: var(--mantine-font-size-lg);
line-height: 1.65;
}
.topic {
color: inherit;
text-decoration: none;
transition:
transform 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease;
}
.topic:hover {
transform: translateY(-2px);
border-color: light-dark(var(--mantine-color-blue-3), var(--mantine-color-blue-7));
box-shadow: var(--mantine-shadow-md);
}
.section {
scroll-margin-top: var(--mantine-spacing-xl);
padding-top: var(--mantine-spacing-xl);
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.sectionTitle,
.contactTitle {
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-family: Outfit, var(--mantine-font-family);
font-weight: 500;
}
.sectionTitle {
font-size: clamp(24px, 3vw, 30px);
}
.contact {
border: 1px solid light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-9));
background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6));
}
.contactTitle {
font-size: 26px;
}
.contactCopy {
max-width: 540px;
}
@media (max-width: 48em) {
.contact :global(.mantine-Group-root) {
align-items: flex-start;
flex-direction: column;
}
}

251
src/pages/SupportPage.tsx Normal file
View File

@@ -0,0 +1,251 @@
import {
Accordion,
Anchor,
Button,
Card,
Group,
Paper,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
} from '@mantine/core';
import {
IconKey,
IconMail,
IconMessageCircle,
IconShieldLock,
type TablerIcon,
} from '@tabler/icons-react';
import type { ReactNode } from 'react';
import { LegalPage } from '../components/LegalPage/LegalPage';
import { SEO } from '../components/SEO/SEO';
import classes from './SupportPage.module.css';
const TOPICS: Array<{
href: string;
icon: TablerIcon;
title: string;
description: string;
}> = [
{
href: '#account',
icon: IconKey,
title: 'Account and access',
description: 'Recovery phrases, device access, and account security.',
},
{
href: '#messaging',
icon: IconMessageCircle,
title: 'Messaging and media',
description: 'Messages, photos, videos, files, voice messages, and groups.',
},
{
href: '#privacy',
icon: IconShieldLock,
title: 'Privacy and safety',
description: 'Security questions, privacy information, and abuse reports.',
},
];
const ACCOUNT_FAQS = [
{
question: 'How do I access my account on a new device?',
answer:
'Install Rosetta and enter your recovery phrase in the exact order it was created. Rosetta may ask you to confirm a new device to protect your account.',
},
{
question: 'I lost my recovery phrase. Can it be restored?',
answer:
'No. Your recovery phrase is the key to your account. Rosetta cannot recover it for you, so keep it private and backed up before you need it.',
},
{
question: 'Why am I asked to confirm a new device?',
answer:
'New-device confirmation helps prevent someone who has obtained a recovery phrase from silently accessing your account on another device.',
},
{
question: 'How do I report an account-access problem?',
answer:
'Email us with the device model, Rosetta version, and a short description of what happened. Do not include your recovery phrase or private keys.',
},
];
const MESSAGING_FAQS = [
{
question: 'How do I send a photo, video, or file?',
answer:
'Open a chat, tap the attachment button next to the message field, choose the item you want to share, then send it. Allow Photos or Files access when iOS asks for it.',
},
{
question: 'Why is an attachment still sending?',
answer:
'Media uploads depend on the file size and your connection. Keep Rosetta open and connected until the send indicator completes. You can retry a message that failed.',
},
{
question: 'How do I send a voice message?',
answer:
'In a chat, press and hold the microphone control to record, then release it to send. Microphone permission is required the first time you record.',
},
{
question: 'How do I get help with a group?',
answer:
'Contact us with the group name and a description of the issue. Do not send private recovery phrases, message contents, or encryption keys.',
},
];
const PRIVACY_FAQS = [
{
question: 'How is message content protected?',
answer:
'Rosetta is designed to encrypt message content before it is sent. For details about how we handle data, read the Rosetta Privacy Policy.',
},
{
question: 'How do I report abuse or a security issue?',
answer: (
<>
Email{' '}
<Anchor href="mailto:admin@rosetta.im?subject=Rosetta%20Security%20Report">
Admin@rosetta.im
</Anchor>{' '}
with a description of the concern. Do not include recovery phrases or private
keys.
</>
),
},
];
interface SupportSectionProps {
id: string;
title: string;
description: string;
items: Array<{ question: string; answer: ReactNode }>;
}
function SupportSection({ id, title, description, items }: SupportSectionProps) {
return (
<Stack component="section" id={id} gap="lg" className={classes.section}>
<div>
<Title order={2} className={classes.sectionTitle}>
{title}
</Title>
<Text c="dimmed" mt="xs" lh={1.65}>
{description}
</Text>
</div>
<Accordion multiple variant="separated" radius="md">
{items.map(({ question, answer }) => (
<Accordion.Item key={question} value={question}>
<Accordion.Control>{question}</Accordion.Control>
<Accordion.Panel>
<Text c="dimmed" size="sm" lh={1.65}>
{answer}
</Text>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
</Stack>
);
}
export function SupportPage() {
return (
<>
<SEO
title="Rosetta Support"
description="Contact Rosetta Support for help with account access, recovery phrases, messaging, media, privacy, and security."
canonical="https://rosetta.im/support/"
keywords="Rosetta support, account recovery, messaging help, privacy help"
/>
<LegalPage
active="support"
eyebrow="Rosetta Support"
title="Help with your Rosetta account and messages."
>
<Stack gap="xl">
<div>
<Text className={classes.lead}>
Find answers about account access, recovery phrases, private messaging,
shared media, and privacy. For anything else, contact our support team.
</Text>
<Button
component="a"
href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request"
leftSection={<IconMail size={18} />}
mt="xl"
size="md"
>
Email Rosetta Support
</Button>
</div>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
{TOPICS.map(({ href, icon: Icon, title, description }) => (
<Card
key={href}
component="a"
href={href}
withBorder
radius="md"
padding="lg"
className={classes.topic}
>
<ThemeIcon variant="light" radius="xl" size="lg">
<Icon size={18} />
</ThemeIcon>
<Text fw={600} mt="md">
{title}
</Text>
<Text c="dimmed" size="sm" mt={6} lh={1.5}>
{description}
</Text>
</Card>
))}
</SimpleGrid>
<SupportSection
id="account"
title="Account and access"
description="Rosetta accounts are accessed with a recovery phrase instead of a phone number. Keep your phrase private and stored somewhere safe."
items={ACCOUNT_FAQS}
/>
<SupportSection
id="messaging"
title="Messaging and media"
description="Rosetta supports private chats, groups, photos, videos, files, and voice messages. Attachment choices can vary depending on your device permissions and the selected item."
items={MESSAGING_FAQS}
/>
<SupportSection
id="privacy"
title="Privacy and safety"
description="Rosetta is designed for private communication. If you have a security concern or a question about data handling, contact us directly."
items={PRIVACY_FAQS}
/>
<Paper radius="lg" p="xl" className={classes.contact}>
<Group justify="space-between" align="center">
<div>
<Title order={2} className={classes.contactTitle}>
Need more help?
</Title>
<Text c="dimmed" mt="xs" className={classes.contactCopy}>
Send us your device model, app version, and a short explanation.
Never send a recovery phrase, private key, or private message
content.
</Text>
</div>
<Anchor
href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request"
fw={700}
>
Admin@rosetta.im
</Anchor>
</Group>
</Paper>
</Stack>
</LegalPage>
</>
);
}

View File

@@ -55,7 +55,7 @@ export function TermsPage() {
<LegalText> <LegalText>
Your use of Rosetta is also governed by our Privacy Policy, available at{' '} Your use of Rosetta is also governed by our Privacy Policy, available at{' '}
<Anchor component={Link} to="/privacy"> <Anchor component={Link} to="/privacy">
https://rosetta.im/privacy/ Privacy Policy
</Anchor> </Anchor>
. .
</LegalText> </LegalText>
@@ -130,10 +130,12 @@ export function TermsPage() {
</LegalText> </LegalText>
<LegalText> <LegalText>
Support contact:{' '} Support contact:{' '}
<Anchor href="mailto:admin@rosetta.im">admin@rosetta.im</Anchor> <Anchor href="mailto:admin@rosetta.im">Admin@rosetta.im</Anchor>
<br /> <br />
Support centre:{' '} Support centre:{' '}
<Anchor href="/support/">https://rosetta.im/support/</Anchor> <Anchor component={Link} to="/support">
Rosetta Support
</Anchor>
</LegalText> </LegalText>
</LegalSection> </LegalSection>
@@ -210,7 +212,7 @@ export function TermsPage() {
{'ROSETTA CLOUD SERVICES LIMITED\nRegistered address:\nUNIT 704C 7/F BLK 3 NAN FUNG IND CITY\n18 TIN HAU RD\nTUEN MUN\nHONG KONG'} {'ROSETTA CLOUD SERVICES LIMITED\nRegistered address:\nUNIT 704C 7/F BLK 3 NAN FUNG IND CITY\n18 TIN HAU RD\nTUEN MUN\nHONG KONG'}
</LegalAddress> </LegalAddress>
<LegalText> <LegalText>
Email: <Anchor href="mailto:admin@rosetta.im">admin@rosetta.im</Anchor> Email: <Anchor href="mailto:admin@rosetta.im">Admin@rosetta.im</Anchor>
</LegalText> </LegalText>
<LegalText> <LegalText>
Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor> Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor>