website: file transfer page (#6644)

* add implementation plan

* website: remove unnecessary libsodium direct dependency from file page plan

* website: update file page plan for async encryption, tailwind, no worker

* add product plan

* update product plan based on the feedback

* remove implementation details from product plan

* update product plan

* add updated implementation plan

* website: add build infrastructure for /file route

* website: fix card click and overlay hash handling for /file page

* website: add /file page with XFTP file transfer and protocol overlay

* website: redesign /file page layout and styling

* fix(website): scope hero h1/h2 font overrides to .hero-section-1

* fix(website): fix /file overlay diagram scaling on short viewports

* style(website): match /file page top padding with /directory

* website: remove file page in navbar

* website: switch xftp-web to official one

* website: fix web.sh

* update texts

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
sh
2026-03-09 16:22:39 +00:00
committed by GitHub
parent 2475a2163a
commit ac62ba4892
16 changed files with 1445 additions and 9 deletions

1
.gitignore vendored
View File

@@ -55,6 +55,7 @@ website/src/img/images/
website/src/images/
website/src/js/lottie.min.js
website/src/js/ethers*
website/src/file-assets/
website/src/privacy.md
# Generated files
website/package/generated*

View File

@@ -0,0 +1,472 @@
# File Transfer Page — Implementation Plan
## Table of Contents
1. [Context](#1-context)
2. [Executive Summary](#2-executive-summary)
3. [High-Level Design](#3-high-level-design)
4. [Detailed Implementation Plan](#4-detailed-implementation-plan)
5. [Known Divergences from Product Plan](#5-known-divergences-from-product-plan)
6. [Verification](#6-verification)
---
## 1. Context
**Problem**: The website needs a `/file` page that lets users upload/download files via XFTP servers directly in the browser — a live demo that funnels users toward downloading the SimpleX app.
**Product plan**: `plans/website-file-page-product.md`
**Approach**: Use the pre-built `dist-web/` bundle from `@shhhum/xftp-web@0.8.0`. Copy three files (`index.js` + `index.css` + `crypto.worker.js`) to website static assets. Wrap with an 11ty page providing the protocol overlay, app download CTA, and i18n bridge. **No Vite/TS build step.** The bundle handles all XFTP protocol, crypto, Web Worker, upload/download UI.
**Library features used** (v0.8.0):
- `data-xftp-app` — configurable target element
- `data-no-hashchange` — prevents conflict with overlay system
- `window.__XFTP_I18N__` — string externalization for i18n
- `xftp:upload-complete` / `xftp:download-complete` — CustomEvents for CTA injection
- Scoped CSS (`#app` / `.dark #app`) — no global resets
- Relative worker URL — both files co-located in same directory
**Routing**: `/file/` (no hash) = upload mode; `/file/#<uri>` = download mode.
---
## 2. Executive Summary
| Action | Files |
|--------|-------|
| **Create** | `website/src/file.html`, `website/src/_data/file_overlays.json`, `website/src/_includes/overlay_content/file/protocol.html` |
| **Copy from npm** | `dist-web/assets/index.js` + `dist-web/assets/index.css` + `dist-web/assets/crypto.worker.js``src/file-assets/` |
| **Modify** | `website/package.json`, `website/.eleventy.js`, `website/src/_includes/navbar.html`, `website/langs/en.json` (~30 keys), `website/web.sh`, `website/src/js/script.js`, `.gitignore` |
---
## 3. High-Level Design
### Architecture
```
website/src/
├── file.html # 11ty page
├── _data/file_overlays.json # overlay config (showImage: false for v1)
├── _includes/overlay_content/file/
│ └── protocol.html # protocol popup content
└── file-assets/ # COPIED from npm dist-web/assets/ (gitignored)
├── index.js # main bundle (~1.1 MB)
├── index.css # scoped CSS (~2.3 KB)
└── crypto.worker.js # worker (~1.0 MB)
```
### Data flow
**Upload**: `#app` div → bundle renders drop zone → file input → Worker encrypts (OPFS) → `uploadFile()` → share link → `xftp:upload-complete` event → website shows inline CTA
**Download**: hash parsed by bundle on init → `decodeDescriptionURI()` → download button → Worker decrypts → browser save → `xftp:download-complete` event → website shows inline CTA
### Overlay conflict resolution
Bundle's `hashchange` listener is disabled via `data-no-hashchange` attribute. Protocol overlay opens via **direct DOM manipulation** (inline JS `classList.remove('hidden')`) — not hash-based. script.js's global `.close-overlay-btn` handler still closes it. No hash events fired when opening.
Note: `closeOverlay()` in script.js calls `history.replaceState(null, null, ' ')` which clears the URL hash. In download mode (`/file/#simplex:...`), this means the hash disappears from the URL bar after closing the overlay. This is cosmetic only — the bundle parses the hash once on init and doesn't re-read it. Download continues unaffected.
A null guard is added to `openOverlay()` in script.js (Step 9) to prevent crashes when the hash is an XFTP URI fragment rather than a DOM element ID.
### i18n bridge
The 11ty template renders `window.__XFTP_I18N__` from en.json keys. The bundle reads via `t(key, fallback)`. All JS-rendered strings are overridable. The bundle renders strings via template literals into innerHTML, so HTML in i18n values (e.g. links in `maxSizeHint`) is rendered correctly.
---
## 4. Detailed Implementation Plan
### Step 1: Add npm dependency
**Modify**: `website/package.json`
```diff
"dependencies": {
+ "@shhhum/xftp-web": "^0.8.0",
}
```
### Step 2: Copy dist-web files in web.sh
**Modify**: `website/web.sh`
After the existing `cp node_modules/...` lines (after line 30):
```bash
mkdir -p src/file-assets
cp node_modules/@shhhum/xftp-web/dist-web/assets/index.js src/file-assets/
cp node_modules/@shhhum/xftp-web/dist-web/assets/index.css src/file-assets/
cp node_modules/@shhhum/xftp-web/dist-web/assets/crypto.worker.js src/file-assets/
```
Add `file.html` to language copy loop (after line 42, `cp src/fdroid.html src/$lang`):
```bash
cp src/file.html src/$lang
```
### Step 3: Create 11ty page — `website/src/file.html`
```
---
layout: layouts/main.html
title: "SimpleX File Transfer"
description: "Send files securely with end-to-end encryption"
templateEngineOverride: njk
active_file: true
---
{% set lang = page.url | getlang %}
{% from "components/macro.njk" import overlay %}
```
**Structure** (top to bottom):
1. **Noscript fallback**:
```html
<noscript>
<p class="text-center text-grey-black dark:text-white py-10">
{{ "file-noscript" | i18n({}, lang) | safe }}
</p>
</noscript>
```
2. **Page section** with centered container:
- `<h1>` with i18n title
- `<div id="app" data-xftp-app data-no-hashchange>` — bundle renders here, hashchange disabled
- Static "E2E encrypted" note below `#app`:
```html
<p class="text-center text-sm text-gray-500 dark:text-gray-400 mt-3">
{{ "file-e2e-note" | i18n({}, lang) | safe }}
</p>
```
- "Learn more" link (opens overlay via inline JS, not hash):
```html
<p class="text-center mt-4">
<a id="learn-more-btn" href="javascript:void(0);" class="text-active-blue hover:underline">
{{ "file-learn-more" | i18n({}, lang) | safe }}
</a>
</p>
```
3. **Inline CTA container** (hidden, shown by JS after upload/download):
```html
<div id="inline-cta" class="hidden text-center py-8">
<h2 class="text-2xl font-bold text-grey-black dark:text-white mb-3">
{{ "file-cta-heading" | i18n({}, lang) | safe }}
</h2>
<p class="text-base text-grey-black dark:text-white mb-6 max-w-[600px] mx-auto">
{{ "file-cta-subheading" | i18n({}, lang) | safe }}
</p>
<div class="flex items-center justify-center gap-4 flex-wrap">
<!-- Same 5 store buttons as join_simplex.html -->
<a href="https://apps.apple.com/us/app/simplex-chat/id1605771084" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apple_store.svg" /></a>
<a href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank"><img class="h-[40px] w-auto" src="/img/new/google_play.svg" /></a>
<a href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid"><img class="h-[40px] w-auto" src="/img/new/f_droid.svg" /></a>
<a href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="h-[40px] w-auto" src="/img/new/testflight.png" /></a>
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
</div>
</div>
```
4. **Protocol overlay** via existing macro:
```html
{% for section in file_overlays.sections %}
{{ overlay(section, lang) }}
{% endfor %}
```
5. **Bottom CTA section** (same pattern as `join_simplex.html`):
- Heading: "Get SimpleX — the most private messenger"
- Subheading about the app using the same protocol
- 5 buttons: Apple Store, Google Play, F-Droid, TestFlight, APK (same markup as inline CTA)
6. **i18n bridge script** (BEFORE bundle load, so `window.__XFTP_I18N__` is set when bundle initializes):
```html
<script>
window.__XFTP_I18N__ = {
"title": "{{ 'file-title' | i18n({}, lang) | safe }}",
"dropZone": "{{ 'file-drop-text' | i18n({}, lang) | safe }}",
"dropZoneHint": "{{ 'file-drop-hint' | i18n({}, lang) | safe }}",
"chooseFile": "{{ 'file-choose' | i18n({}, lang) | safe }}",
"maxSizeHint": "{{ 'file-max-size' | i18n({}, lang) | safe }}",
"encrypting": "{{ 'file-encrypting' | i18n({}, lang) | safe }}",
"uploading": "{{ 'file-uploading' | i18n({}, lang) | safe }}",
"cancel": "{{ 'file-cancel' | i18n({}, lang) | safe }}",
"fileUploaded": "{{ 'file-uploaded' | i18n({}, lang) | safe }}",
"copy": "{{ 'file-copy' | i18n({}, lang) | safe }}",
"copied": "{{ 'file-copied' | i18n({}, lang) | safe }}",
"share": "{{ 'file-share' | i18n({}, lang) | safe }}",
"expiryHint": "{{ 'file-expiry' | i18n({}, lang) | safe }}",
"securityNote1": "{{ 'file-sec-1' | i18n({}, lang) | safe }}",
"securityNote2": "{{ 'file-sec-2' | i18n({}, lang) | safe }}",
"securityNote3": "{{ 'file-sec-3' | i18n({}, lang) | safe }}",
"retry": "{{ 'file-retry' | i18n({}, lang) | safe }}",
"downloading": "{{ 'file-downloading' | i18n({}, lang) | safe }}",
"decrypting": "{{ 'file-decrypting' | i18n({}, lang) | safe }}",
"downloadComplete": "{{ 'file-download-complete' | i18n({}, lang) | safe }}",
"download": "{{ 'file-download-btn' | i18n({}, lang) | safe }}",
"fileTooLarge": "{{ 'file-too-large' | i18n({}, lang) | safe }}",
"fileEmpty": "{{ 'file-empty' | i18n({}, lang) | safe }}",
"invalidLink": "{{ 'file-invalid-link' | i18n({}, lang) | safe }}",
"initError": "{{ 'file-init-error' | i18n({}, lang) | safe }}",
"fileAvailable": "{{ 'file-available' | i18n({}, lang) | safe }}",
"dlSecurityNote1": "{{ 'file-dl-sec-1' | i18n({}, lang) | safe }}",
"dlSecurityNote2": "{{ 'file-dl-sec-2' | i18n({}, lang) | safe }}",
"dlSecurityNote3": "{{ 'file-dl-sec-3' | i18n({}, lang) | safe }}",
"workersRequired": "{{ 'file-workers-required' | i18n({}, lang) | safe }}"
}
</script>
```
7. **Overlay open + CTA injection script**:
```html
<script>
document.getElementById('learn-more-btn')?.addEventListener('click', function() {
var el = document.getElementById('xftp-protocol');
if (el) { el.classList.remove('hidden'); el.classList.add('flex'); document.body.classList.add('lock-scroll'); }
});
document.getElementById('app')?.addEventListener('xftp:upload-complete', function() {
document.getElementById('inline-cta')?.classList.remove('hidden');
});
document.getElementById('app')?.addEventListener('xftp:download-complete', function() {
document.getElementById('inline-cta')?.classList.remove('hidden');
});
</script>
```
8. **Bundle + CSS** (bundle AFTER i18n bridge):
```html
<link rel="stylesheet" href="/file-assets/index.css">
<script type="module" src="/file-assets/index.js"></script>
```
### Step 4: Create protocol overlay data + content
**New file**: `website/src/_data/file_overlays.json`
```json
{
"sections": [{
"id": 1,
"imgLight": "",
"imgDark": "",
"overlayContent": {
"overlayId": "xftp-protocol",
"overlayScrollTo": "",
"title": "file-protocol-title",
"showImage": false,
"contentBody": "overlay_content/file/protocol.html"
}
}]
}
```
Note: `showImage: false` — protocol diagram SVGs are deferred to a future iteration. The overlay works without images (same as existing overlays when `showImage` is false — the content section spans full width).
**New file**: `website/src/_includes/overlay_content/file/protocol.html`
5 blocks with heading + paragraph structure (existing hero overlay cards use plain `<p>` tags; this overlay uses `<h3>` + `<p>` inside `<div>` wrappers since it has titled sections):
```html
<div>
<h3 class="font-bold text-lg mb-2">{{ "file-proto-h-1" | i18n({}, lang) | safe }}</h3>
<p>{{ "file-proto-p-1" | i18n({}, lang) | safe }}</p>
</div>
<div>
<h3 class="font-bold text-lg mb-2">{{ "file-proto-h-2" | i18n({}, lang) | safe }}</h3>
<p>{{ "file-proto-p-2" | i18n({}, lang) | safe }}</p>
</div>
<div>
<h3 class="font-bold text-lg mb-2">{{ "file-proto-h-3" | i18n({}, lang) | safe }}</h3>
<p>{{ "file-proto-p-3" | i18n({}, lang) | safe }}</p>
</div>
<div>
<h3 class="font-bold text-lg mb-2">{{ "file-proto-h-4" | i18n({}, lang) | safe }}</h3>
<p>{{ "file-proto-p-4" | i18n({}, lang) | safe }}</p>
</div>
<div>
<h3 class="font-bold text-lg mb-2">{{ "file-proto-h-5" | i18n({}, lang) | safe }}</h3>
<p>{{ "file-proto-p-5" | i18n({}, lang) | safe }}</p>
</div>
<p class="mt-4">
<a href="https://github.com/simplex-chat/simplexmq/blob/stable/protocol/xftp.md" target="_blank" rel="noopener" class="text-active-blue hover:underline">
{{ "file-proto-spec" | i18n({}, lang) | safe }}
</a>
</p>
```
### Step 5: Add navbar link
**Modify**: `website/src/_includes/navbar.html`
After Directory `<li>` block (after line 27, before the `<hr>` at line 29):
```html
<hr>
<li class="nav-link {% if active_file %}active{% endif %}">
<a href="/file/">
<span class="nav-link-text">{{ "file" | i18n({}, lang) | safe }}</span>
</a>
</li>
```
Add `and ('file' not in page.url)` to language-selector exclusion condition (line 137):
```
{% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('vouchers' not in page.url) and ('file' not in page.url) %}
```
### Step 6: Add translation keys
**Modify**: `website/langs/en.json` — add these keys:
```
Navbar:
"file": "File"
Noscript + static page content:
"file-noscript": "JavaScript is required for file transfer."
"file-e2e-note": "End-to-end encrypted — the server never sees your file."
"file-learn-more": "Learn more about XFTP protocol"
"file-cta-heading": "Get SimpleX — the most private messenger"
"file-cta-subheading": "The file transfer you just used is built on the same protocol as SimpleX Chat — end-to-end encrypted messaging, voice and video calls, groups, and file sharing. No user IDs. No phone numbers."
i18n bridge (fed to bundle via window.__XFTP_I18N__):
"file-title": "SimpleX File Transfer"
"file-drop-text": "Drag & drop a file here"
"file-drop-hint": "or"
"file-choose": "Choose file"
"file-max-size": "Max 100 MB — the <a href=\"#join-simplex\">SimpleX app</a> supports up to 1 GB"
"file-encrypting": "Encrypting\u2026"
"file-uploading": "Uploading\u2026"
"file-cancel": "Cancel"
"file-uploaded": "File uploaded"
"file-copy": "Copy"
"file-copied": "Copied!"
"file-share": "Share"
"file-expiry": "Files are typically available for 48 hours."
"file-sec-1": "Your file was encrypted in the browser before upload — the server never sees file contents."
"file-sec-2": "The link contains the decryption key in the hash fragment, which the browser never sends to any server."
"file-sec-3": "For maximum security, use the <a href=\"https://simplex.chat\" target=\"_blank\" rel=\"noopener\">SimpleX app</a>."
"file-retry": "Retry"
"file-downloading": "Downloading\u2026"
"file-decrypting": "Decrypting\u2026"
"file-download-complete": "Download complete"
"file-download-btn": "Download"
"file-too-large": "File too large (%size%). Maximum is 100 MB. The <a href=\"#join-simplex\">SimpleX app</a> supports files up to 1 GB."
"file-empty": "File is empty."
"file-invalid-link": "Invalid or corrupted link."
"file-init-error": "Failed to initialize: %error%"
"file-available": "File available (~%size%)"
"file-dl-sec-1": "This file is encrypted \u2014 the server never sees file contents."
"file-dl-sec-2": "The decryption key is in the link\u2019s hash fragment, which your browser never sends to any server."
"file-dl-sec-3": "For maximum security, use the <a href=\"https://simplex.chat\" target=\"_blank\" rel=\"noopener\">SimpleX app</a>."
"file-workers-required": "Web Workers required \u2014 update your browser"
Protocol overlay content:
"file-protocol-title": "Why XFTP is the most private file transfer"
"file-proto-h-1": "No accounts, no identifiers"
"file-proto-p-1": "Each file chunk uses a fresh, random credential that is used once and discarded. The server has no concept of \"users\" — it only sees isolated, anonymous chunk operations."
"file-proto-h-2": "Encrypted in your browser"
"file-proto-p-2": "The entire file is encrypted with a random key before upload. The server stores ciphertext it cannot decrypt. The key travels only in the URL fragment, which browsers never send to any server."
"file-proto-h-3": "Triple encryption"
"file-proto-p-3": "Every transfer has three layers: TLS transport encryption, per-recipient transit encryption (unique ephemeral key exchange per download), and file-level end-to-end encryption."
"file-proto-h-4": "Distributed across independent servers"
"file-proto-p-4": "File chunks are split across servers operated by independent parties. No single operator sees all chunks. Even if one operator is compromised, they only see encrypted fragments."
"file-proto-h-5": "Files expire automatically"
"file-proto-p-5": "Files are deleted after approximately 48 hours. There is no persistent storage, no file management, no way to extend expiration. Ephemeral by design."
"file-proto-spec": "Read the XFTP protocol specification →"
```
### Step 7: Update .eleventy.js
**Modify**: `website/.eleventy.js`
1. Add `"file"` to `supportedRoutes` array (line 56):
```js
const supportedRoutes = ["blog", "contact", "invitation", "messaging", "docs", "fdroid", "file", ""]
```
2. Add passthrough copy (after line 306, with the other `addPassthroughCopy` calls):
```js
ty.addPassthroughCopy("src/file-assets")
```
### Step 8: Gitignore
**Modify**: `.gitignore` (project root) — add:
```
website/src/file-assets/
```
### Step 9: Fix script.js null guard
**Modify**: `website/src/js/script.js`
The `openOverlay()` function (line 180) crashes when the URL hash is an XFTP URI fragment (e.g. `#simplex:...`) because `document.getElementById('simplex:...')` returns null, and `el.classList.contains('overlay')` throws a TypeError on null.
**Change** (line 184-185):
```js
// Before:
const el = document.getElementById(id)
if (el.classList.contains('overlay')) {
// After:
const el = document.getElementById(id)
if (el && el.classList.contains('overlay')) {
```
This is a one-character change (`if (el.classList` → `if (el && el.classList`). It makes `openOverlay()` safely ignore hash fragments that don't correspond to overlay elements — which is correct behavior regardless of the file page (any non-overlay hash should be silently ignored).
---
## 5. Known Divergences from Product Plan
These are intentional deviations from the product plan, caused by browser constraints or library limitations:
1. **Download requires a click**: Product plan says "No intermediate 'click to download' step." The bundle shows a "Download" button instead of auto-starting. This is a browser security constraint — triggering a file download requires a user gesture. The button also lets the user see file metadata before downloading.
2. **No cancel during download**: Product plan specifies a Cancel button during download. The bundle does not implement this. The download is relatively fast (direct HTTPS) and cancellation can be done by closing the tab.
3. **Protocol diagram deferred**: Product plan describes a protocol flow diagram in the overlay. SVG diagrams are deferred to a future iteration. The overlay ships with text-only content (`showImage: false`).
4. **Overlay close clears download hash**: When the protocol overlay is opened and closed during download mode, `closeOverlay()` clears the URL hash. This is cosmetic — the bundle already parsed the hash on init and the download is unaffected. The URL bar loses the fragment, but the user received the link from elsewhere and doesn't need to re-copy it.
---
## 6. Verification
### Build
```bash
cd website
npm install --ignore-scripts
mkdir -p src/file-assets
cp node_modules/@shhhum/xftp-web/dist-web/assets/{index.js,index.css,crypto.worker.js} src/file-assets/
npm run build
ls _site/file/index.html _site/file-assets/index.js _site/file-assets/index.css _site/file-assets/crypto.worker.js
```
### Manual test checklist
```
Visit /file/
1. Navbar "File" link is active
2. <noscript> message hidden (JS enabled)
3. Drop zone visible, dark mode works
4. "End-to-end encrypted" note visible below drop zone
5. maxSizeHint shows "Max 100 MB — the SimpleX app supports up to 1 GB" with link
6. Upload small file → progress ring → share link + Copy/Share buttons
7. Inline CTA appears after upload with 5 app store buttons
8. Copy link → new tab → download page → Download button → file saved
9. Inline CTA appears after download
10. "Learn more" → overlay opens (no hash change, no console error)
11. Overlay shows 5 protocol blocks + spec link
12. Close overlay → file tool unaffected, no JS error
13. Bottom CTA shows 5 app store buttons
14. Visit /file/#invalid → no JS crash (null guard works)
15. Language selector hidden on /file/ page
```
### No regressions
```bash
# Verify other pages still build
ls _site/index.html _site/directory/index.html _site/blog/index.html
# Verify overlay still works on homepage (openOverlay null guard is backward-compatible)
```

View File

@@ -0,0 +1,309 @@
# SimpleX File Transfer — Product Plan
## Table of Contents
1. [Strategic purpose](#1-strategic-purpose)
2. [Users and conversion funnel](#2-users-and-conversion-funnel)
3. [Why XFTP is the most private file transfer protocol](#3-why-xftp-is-the-most-private-file-transfer-protocol)
4. [Page structure and UX](#4-page-structure-and-ux)
5. [Upload flow](#5-upload-flow)
6. [Download flow](#6-download-flow)
7. [Edge cases](#7-edge-cases)
8. [Abuse and moderation](#8-abuse-and-moderation)
9. [What this is NOT](#9-what-this-is-not)
---
## 1. Strategic purpose
This page is a **conversion point**. Its primary goal is to get people to download the SimpleX app.
The file transfer is a **live demo** — proof that SimpleX's encryption and privacy infrastructure actually works, right in the browser, without installing anything. The user experiences the technology firsthand: they drop a file, it gets encrypted in their browser, uploaded to XFTP servers with no accounts or identifiers, and the recipient downloads it with a link that contains the decryption key in the URL fragment (which browsers never send to any server).
After experiencing this, the page makes the case: *what you just used is a tiny fraction of what the SimpleX app does. The app uses the same protocol — and much more — for messaging, calls, groups, and file sharing. No user IDs. No phone numbers. The most private communication platform that exists.*
**The page serves three functions, in priority order:**
1. **Demonstrate** — let people experience XFTP encryption firsthand, proving it works
2. **Educate** — explain why the XFTP protocol is the most private file transfer protocol in existence
3. **Convert** — drive app downloads with a clear, compelling call to action
The file transfer tool by itself is useful — but it exists to showcase the protocol and funnel users to the app.
---
## 2. Users and conversion funnel
### Who arrives at this page
**Path 1: Referred by a SimpleX user.** Someone sends them a file link. They click it, download a file, and land on a page that explains what just happened and why they should get the app. This is the highest-intent path — they've already interacted with the SimpleX ecosystem.
**Path 2: Privacy-curious visitors.** People browsing simplex.chat who click "File" in the navbar. They're already interested in SimpleX, and the file transfer demo gives them something to try immediately, without commitment.
**Path 3: Linked from external sources.** Privacy advocates, journalists, security researchers who share the page as "the most private way to send a file without installing anything." These users become evangelists.
### The conversion funnel
```
See the page → Try the demo → Read why it's secure → Download the app
```
Each step must flow naturally into the next. The demo creates curiosity ("how does this work?"), the protocol explanation builds trust ("this is genuinely private"), and the CTA captures intent ("I want this for all my communication").
### Concrete scenarios
- A journalist receives a file link from a source. After downloading, they see the security explanation and realize SimpleX is what they need for source communication. They download the app.
- A privacy-conscious person uploads a file to share medical records. While waiting for the upload, they read the protocol section. They're impressed and download the app.
- A tech blogger finds the page, tries it, and writes about it — linking to the page and driving more traffic into the funnel.
- A SimpleX user sends a file to a friend who doesn't have the app. The friend downloads the file, reads "this is what SimpleX does — and the app does much more," and installs it.
---
## 3. Why XFTP is the most private file transfer protocol
This section defines the protocol properties that the page must communicate to users. These are the facts that make the case for downloading the app.
### No user identifiers at any level
XFTP has no accounts, no usernames, no email addresses, no phone numbers, no device tokens, no persistent IDs of any kind. Not even random numbers. Each file chunk uses a fresh, random credential that is used once and discarded. The server has no concept of "users" — it only sees isolated, anonymous chunk operations.
This is not a feature bolted onto an existing system. The protocol was designed from the ground up to have no identifiers. SimpleX is the first communication platform to achieve this.
### Triple encryption
Every file transfer has three independent layers of encryption:
1. **TLS transport** — standard HTTPS encryption for the network connection
2. **Per-recipient transit encryption** — each download uses a unique ephemeral key exchange, so the ciphertext is different for every recipient, even for the same chunk. There are no identifiers or ciphertext in common between sent and received traffic — even if TLS is compromised, traffic correlation is frustrated.
3. **File-level end-to-end encryption** — the entire file is encrypted with a random key before upload. The key is in the URL fragment, which browsers never send to any server.
No other file transfer service has all three layers. Most have only TLS.
### Traffic correlation resistance
The protocol is designed so that even if TLS is compromised, an attacker monitoring server traffic cannot easily correlate senders and recipients:
- **No shared ciphertext**: each recipient's download is encrypted with a unique key. The bytes going in (upload) and coming out (download) are completely different, even for the same chunk.
- **No shared identifiers**: sender and recipient IDs are different random values for every chunk.
- **Fixed chunk sizes**: files are split into fixed-size chunks. A large file looks indistinguishable from many small files to the server.
### Multi-operator, zero-trust architecture
File chunks are distributed across servers operated by independent parties (SimpleX and Flux). No single operator sees all chunks of a file. Even if one operator is compromised, they only see encrypted fragments with no way to reconstruct the file.
Users can also self-host XFTP servers — the protocol is open and the server software is open-source.
### Deniability
Recipients cannot cryptographically prove to a third party that a file came from a specific sender. Two contacts cannot collaborate to confirm they are communicating with the same person, even if they receive the same file. This is a protocol-level guarantee, not a policy.
### Protocol-mandated privacy
The XFTP protocol specification *requires* that servers:
- Do NOT log client commands or transport connections
- Do NOT store history of retrieved files
- Do NOT create database snapshots
- Do NOT store any information that may compromise privacy or forward secrecy
This is not a "we promise not to log" policy — it's a protocol requirement that any compliant server implementation must follow.
### Ephemeral by design
Files expire automatically (approximately 48 hours). There is no persistent storage, no file management, no way to extend expiration. The system is designed for transient transfer, not storage. A breach years later finds nothing.
### How this compares
| Property | XFTP (SimpleX) | WeTransfer | Google Drive | Signal | OnionShare |
|----------|----------------|------------|-------------|--------|------------|
| No accounts required | Yes | No | No | No (phone number) | Yes |
| No user identifiers | Yes | No | No | No | Partial |
| E2E encryption | Yes (3 layers) | No | No | Yes (1 layer) | Yes (1 layer) |
| Server cannot read files | Yes | No | No | Yes | Yes |
| Traffic correlation resistance | Yes | No | No | No | Partial (Tor) |
| Multi-operator distribution | Yes | No | No | No | No |
| No installation required (sender) | Yes | No | No | No | No |
| No installation required (recipient) | Yes | Yes | Yes | No | No |
| Deniability | Yes | No | No | Yes | No |
| Open protocol specification | Yes | No | No | Yes | Yes |
| Auto-expiring files | Yes | Partial | No | N/A | Yes |
---
## 4. Page structure and UX
### Design principles
1. **Demo first, educate second, convert third.** The file transfer tool is at the top. The protocol explanation is available via a "Learn more" popup. The app download CTA is at the bottom (and repeated after upload/download completes). The user experiences before they learn, and learns before they're asked to act.
2. **Zero decisions.** The upload/download tool has no settings, no options, no configuration. Drop a file, get a link. Click a link, get a file.
3. **Transparency without jargon.** Show what's happening (encrypting, uploading, done) and explain the security properties in plain language. Technical users can read the protocol spec linked at the bottom.
4. **Match the website.** Same navbar, dark mode, typography. The file page is a natural part of simplex.chat.
### Page layout (top to bottom)
**1. Title**: "SimpleX File Transfer"
**2. File transfer tool**: The upload drop zone or download progress — this is the interactive demo. After a successful upload or download, an inline app download CTA appears directly below the completion state.
**3. "Learn more" link → protocol popup**
Below the file transfer tool, a "Learn more about XFTP protocol" link. Clicking it opens a modal/popup overlay explaining the XFTP protocol security properties. The popup contains:
**A protocol diagram** showing the XFTP file transfer flow:
- Sender encrypts file in browser → splits into chunks → uploads to multiple independent XFTP servers (SimpleX + Flux)
- Recipient clicks link → downloads chunks from servers → decrypts in browser
- Key visual points: the encryption key travels in the URL fragment (never sent to servers), each chunk uses unique anonymous credentials, each recipient download is re-encrypted with a unique key
**Below the diagram**, 4-5 short scannable blocks with bold titles:
- **No accounts, no identifiers** — one sentence explaining anonymous per-chunk credentials
- **Encrypted in your browser** — the server stores ciphertext it cannot decrypt; the key is in the link
- **Triple encryption** — TLS + per-recipient transit encryption + file-level E2E
- **Distributed across independent servers** — chunks split across SimpleX and Flux operators; no single operator sees the complete file
- **Files expire automatically** — approximately 48 hours, ephemeral by design
Each block is 2-3 sentences.
A link at the bottom of the popup: "Read the XFTP protocol specification for the full technical details."
The popup uses the same overlay pattern as existing website popups (the "Why user IDs are bad for privacy?" and "How does SimpleX work?" overlays on the homepage).
**4. App download CTA**
The conversion section. Clear, prominent, centered:
- Heading: "Get SimpleX — the most private messenger"
- Subheading: "The file transfer you just used is built on the same protocol as SimpleX Chat — end-to-end encrypted messaging, voice and video calls, groups, and file sharing. No user IDs. No phone numbers."
- App store buttons (Apple Store, Google Play, F-Droid, TestFlight, APK)
This section appears at the bottom of the page AND is shown inline after a successful upload or download.
---
## 5. Upload flow
### State 1: Ready (drop zone)
A centered card with:
- A drop zone with a dashed border — "Drag & drop a file here"
- The word "or" in muted text
- A "Choose file" button (for mobile and accessibility)
- "Max 100 MB — the SimpleX app supports up to 1 GB" as a hint below, where "SimpleX app" links to the app download section
Below the drop zone: "End-to-end encrypted — the server never sees your file."
When the user drags a file over, the border highlights.
### State 2: Encrypting + uploading (progress)
The drop zone is replaced by:
- A circular progress indicator
- Status text: "Encrypting..." then "Uploading..."
- A "Cancel" button
Cancel returns to the drop zone. No confirmation dialog.
### State 3: Complete (share link + CTA)
- "File uploaded" in success styling
- A text input containing the full share URL, pre-selected for easy copying
- A "Copy" button — clicking changes text to "Copied!" for 2 seconds
- On mobile: a "Share" button that opens the native share sheet (WhatsApp, Telegram, AirDrop, etc.). Falls back to Copy if unavailable.
- "Files are typically available for 48 hours."
Security note (three short lines):
- "Your file was encrypted in the browser before upload — the server never sees file contents."
- "The link contains the decryption key — your browser never sends it to any server."
- "For maximum security, use the SimpleX app."
**Inline CTA** below the security note — same prominence as the bottom-of-page CTA:
- Heading: "Get SimpleX — the most private messenger"
- Subheading about the app using the same protocol
- App store buttons
### State 4: Error
- An error message in plain language
- A "Retry" button for retriable errors (network timeout, connection reset)
- No retry button for permanent errors (file too large, empty file, server rejected)
- Retry re-attempts with the same file — no re-selection needed
---
## 6. Download flow
### State 1: Downloading (progress)
When the page loads with a file link, it immediately starts downloading:
- A circular progress indicator (same visual as upload)
- Status text: "Downloading..."
- A "Cancel" button
No intermediate "click to download" step. The user clicked the link — start immediately.
### State 2: Complete (save + CTA)
- The browser's native save dialog with the original filename
- "File downloaded" confirmation
- The same security note as the upload page
**Inline CTA** — same as upload completion. Same heading, subheading, and app store buttons. This is the highest-conversion moment — the user just experienced the technology working and is most receptive.
### State 3: Error
- "This file is no longer available" — for expired/deleted files (permanent, no retry)
- "Download failed" with retry — for network errors (retriable)
- "Invalid link — the file link appears to be incomplete or corrupted." — for malformed links (permanent, no retry)
---
## 7. Edge cases
**File too large (>100 MB):** Error shown immediately after file selection. "File too large (X MB). Maximum is 100 MB. The SimpleX app supports files up to 1 GB." — where "SimpleX app" links to the app download section. This is also a conversion opportunity.
**Empty file (0 bytes):** Rejected immediately. "File is empty."
**JavaScript disabled:** A message saying "JavaScript is required."
**Slow connection:** Progress indicator provides continuous feedback. Cancel is always available. No silent timeouts.
**Browser back/forward:** Back button works naturally — returns to upload mode.
**Mobile:** "Choose file" button triggers native file picker. Drag & drop is desktop-only. Layout is responsive.
**Dark mode:** Follows the website's existing dark mode toggle.
**Malformed link:** "Invalid link — the file link appears to be incomplete or corrupted." No retry button. Distinct from "file not found" (expired) so the user knows whether to ask for a new link.
**Link truncation:** Long URLs can be truncated by SMS, Twitter/X, some email clients. The protocol's redirect compression mitigates this for larger files. Small files have naturally short URLs.
**Internationalization:** All UI strings go through the website's existing i18n system (translation keys in language JSON files). The page supports all languages the website supports.
---
## 8. Abuse and moderation
The server stores encrypted bytes and cannot inspect file contents. This is the core privacy guarantee.
Mitigations:
- **48-hour expiration** — files are automatically deleted. Not suitable for persistent hosting.
- **Rate limiting** — XFTP servers enforce per-IP upload rate limits and storage quotas.
- **No directory or discovery** — no public listing, no search. You need the exact link.
- **Abuse reporting** — can be added later. Server operators can delete specific file chunks without decrypting.
- **Legal compliance** — server operators handle takedown requests under their jurisdictions without compromising E2E encryption of other files.
Same trade-off as Signal, WhatsApp, iMessage. Ephemeral nature (48h) significantly reduces risk vs. permanent storage.
---
## 9. What this is NOT
- **Not a file storage service.** Files expire. No dashboard, no management.
- **Not a collaboration tool.** One sender, one link, one or more recipients.
- **Not the destination.** This is the on-ramp to SimpleX Chat. The demo proves the technology. The app is the product.

View File

@@ -53,7 +53,7 @@ const globalConfig = {
}
const translationsDirectoryPath = './langs'
const supportedRoutes = ["blog", "contact", "invitation", "messaging", "docs", "fdroid", "why", ""]
const supportedRoutes = ["blog", "contact", "invitation", "messaging", "docs", "fdroid", "why", "file", ""]
let supportedLangs = []
fs.readdir(translationsDirectoryPath, (err, files) => {
if (err) {
@@ -305,6 +305,7 @@ module.exports = function (ty) {
ty.addPassthroughCopy("src/images")
ty.addPassthroughCopy("src/CNAME")
ty.addPassthroughCopy("src/.well-known")
ty.addPassthroughCopy("src/file-assets")
ty.addCollection('blogs', function (collection) {
return collection.getFilteredByGlob('src/blog/*.md').reverse()
@@ -452,4 +453,4 @@ module.exports = function (ty) {
htmlTemplateEngine: 'njk',
dataTemplateEngine: 'njk',
}
}
}

View File

@@ -324,5 +324,48 @@
"why-p7": "The oldest human freedom &mdash; to speak to another person without being watched &mdash; built on infrastructure that cannot betray it.",
"why-p8": "Because we destroyed the power to know who you are. So that <em>your</em> power can never be taken.",
"why-tagline": "Be free in your network.",
"why-footer-link": "Why we are building it"
"why-footer-link": "Why we are building it",
"file": "File",
"file-desc": "Send files securely with end-to-end encryption — no accounts, no tracking.",
"file-noscript": "JavaScript is required for file transfer.",
"file-e2e-note": "End-to-end encrypted — the server never sees your file.",
"file-learn-more": "Learn more about XFTP protocol",
"file-cta-heading": "Get SimpleX Chat &mdash; the most secure &amp; private messenger",
"file-cta-subheading": "The file transfer you just used uses the same data routing protocol as SimpleX Chat. The app has end-to-end encrypted messaging, voice and video calls, groups, and sending files. No account. No phone. No email. No user profile IDs.",
"file-title": "SimpleX File Transfer",
"file-drop-text": "Drag & drop a file here",
"file-drop-hint": "or",
"file-choose": "Choose file",
"file-max-size": "Max 100 MB - <a href=\"#join-simplex\">SimpleX Chat app</a> supports files up to 1 GB",
"file-encrypting": "Encrypting\u2026",
"file-uploading": "Uploading\u2026",
"file-cancel": "Cancel",
"file-uploaded": "File uploaded",
"file-copy": "Copy",
"file-copied": "Copied!",
"file-share": "Share",
"file-expiry": "Files are typically available for 48 hours.",
"file-sec-1": "Your file was encrypted in the browser - data routers never see file contents, name or size.",
"file-sec-2": "The encryption key is in the link\u2019s hash fragment - it is never sent to any server.",
"file-sec-3": "For better security, use <a href=\"https://simplex.chat/download\">SimpleX Chat</a> app.",
"file-retry": "Retry",
"file-downloading": "Downloading\u2026",
"file-decrypting": "Decrypting\u2026",
"file-download-complete": "Download complete",
"file-download-btn": "Download",
"file-too-large": "File too large (%size%). Maximum is 100 MB. The <a href=\"#join-simplex\">SimpleX app</a> supports files up to 1 GB.",
"file-empty": "File is empty.",
"file-invalid-link": "Invalid or corrupted link.",
"file-init-error": "Failed to initialize: %error%",
"file-available": "File available (~%size%)",
"file-dl-sec-1": "This file is encrypted - data routers never see file contents, name or size.",
"file-workers-required": "Web Workers required \u2014 update your browser",
"file-protocol-title": "XFTP protocol: the most secure file transfer",
"file-proto-h-1": "No account required",
"file-proto-p-1": "Each file fragment uses a new random key. Data routers have no \"users\" or \"files\" - they transfer encrypted file fragments of fixed sizes.",
"file-proto-h-2": "Triple-encrypted in your browser",
"file-proto-p-2": "File encryption key is present only in the URL hash fragment - your browser never sends it to a server. There are 3 encryption layers: TLS transport, per-recipient encryption (unique ephemeral key per transfer), and file end-to-end encryption.",
"file-proto-h-4": "Independent data routers",
"file-proto-p-4": "When file is split to fragments, it is sent via network routers operated by independent parties. No operator can see the actual file size or name. Even if a router is compromised, it can only see encrypted fragments of fixed size. File fragments are cached by network routers for approximately 48 hours.",
"file-proto-spec": "Read the XFTP protocol specification →"
}

View File

@@ -29,6 +29,7 @@
"tailwindcss": "3.3.1"
},
"dependencies": {
"@simplex-chat/xftp-web": "^0.2.0",
"eleventy-plugin-i18n": "^0.1.3",
"fs": "^0.0.1-security",
"gray-matter": "^4.0.3",

View File

@@ -0,0 +1,16 @@
{
"sections": [
{
"id": 1,
"imgLight": "/img/new/xftp-protocol.svg",
"imgDark": "/img/new/xftp-protocol-dark.svg",
"overlayContent": {
"overlayId": "xftp-protocol",
"overlayScrollTo": "",
"title": "file-protocol-title",
"showImage": true,
"contentBody": "overlay_content/file/protocol.html"
}
}
]
}

View File

@@ -26,6 +26,14 @@
</a>
</li>
{# <hr>
<li class="nav-link {% if active_file %}active{% endif %}">
<a href="/file/">
<span class="nav-link-text">{{ "file" | i18n({}, lang) | safe }}</span>
</a>
</li> #}
<hr>
<li class="nav-link">
@@ -134,7 +142,7 @@
</svg>
</button>
{% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('vouchers' not in page.url) %}
{% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('vouchers' not in page.url) and ('file' not in page.url) %}
<div class="nav-link flag-container">
<a href="javascript:void(0);">
{% for language in languages.languages %}

View File

@@ -0,0 +1,17 @@
<div>
<h3 class="font-bold text-lg mb-2">{{ "file-proto-h-1" | i18n({}, lang) | safe }}</h3>
<p>{{ "file-proto-p-1" | i18n({}, lang) | safe }}</p>
</div>
<div>
<h3 class="font-bold text-lg mb-2">{{ "file-proto-h-2" | i18n({}, lang) | safe }}</h3>
<p>{{ "file-proto-p-2" | i18n({}, lang) | safe }}</p>
</div>
<div>
<h3 class="font-bold text-lg mb-2">{{ "file-proto-h-4" | i18n({}, lang) | safe }}</h3>
<p>{{ "file-proto-p-4" | i18n({}, lang) | safe }}</p>
</div>
<p class="mt-4">
<a href="https://github.com/simplex-chat/simplexmq/blob/stable/protocol/xftp.md" target="_blank" rel="noopener" class="text-active-blue hover:underline">
{{ "file-proto-spec" | i18n({}, lang) | safe }}
</a>
</p>

View File

@@ -976,10 +976,10 @@ p a{
height: 100%;
}
h1{
.hero-section-1 h1{
font-size: 48px !important;
}
h2{
.hero-section-1 h2{
font-size: 36px !important;
}
}

316
website/src/file.html Normal file
View File

@@ -0,0 +1,316 @@
---
layout: layouts/main.html
title: "SimpleX File Transfer"
description: "Send files securely with end-to-end encryption"
templateEngineOverride: njk
active_file: true
---
{% set lang = page.url | getlang %}
{% from "components/macro.njk" import overlay %}
<link rel="stylesheet" href="/file-assets/index.css">
<style>
/* Reset website .card styles that conflict with xftp bundle */
#app .card { cursor: default; }
#app .card,
#app .card > div { transition: unset; height: auto; }
#app .card > div > * { opacity: unset; max-height: unset; transform: unset; transition: unset; }
/* Align xftp bundle with website design */
#app { font-family: inherit; color: #3F484B; max-width: 600px; width: 100%; margin: 0 auto; padding: 0; }
#app .card { background: transparent; box-shadow: none; padding: 0; border-radius: 0; }
#app .card > h1 { display: none; }
.dark #app h1 { color: #fff; }
#app .drop-zone { border: 2px dashed #c5d3de; border-radius: 12px; padding: 40px 24px; }
#app .drop-zone.drag-over { border-color: #0053D0; background: #DBEEFF; }
#app .btn { background: #0053D0; border-radius: 34px; padding: 12px 32px; font-size: .9rem; font-weight: 600; }
#app .btn:hover { background: #1661D1; }
#app .btn-secondary { background: #6b7280; border-radius: 34px; }
#app .hint { color: #6b7280; }
#app .link-row input { border-radius: 8px; border-color: #c5d3de; }
#app .security-note { background: #ffffff; border-radius: 12px; color: #3F484B; }
#app .security-note a { color: #0197FF; }
#app .success { color: #0053D0; }
.dark #app { color: #e5e7eb; }
.dark #app .card { background: transparent; box-shadow: none; }
.dark #app .drop-zone { border-color: #374151; }
.dark #app .drop-zone.drag-over { border-color: #70F0F9; background: #1B325C; }
.dark #app .btn { background: #70F0F9; color: #000832; }
.dark #app .btn:hover { background: #66D9E2; }
.dark #app .link-row input { border-color: #374151; background: #071C46; color: #e5e7eb; }
.dark #app .security-note { background: #0B2A59; color: #d1d5db; }
.dark #app .security-note a { color: #70F0F9; }
.dark #app .success { color: #70F0F9; }
#app #upload-error { border: 2px dashed #c5d3de; border-radius: 12px; padding: 40px 24px; text-align: center; }
.dark #app #upload-error { border-color: #374151; }
#app .stage > .btn { margin-top: 20px; }
#app .security-note { margin-top: 20px; }
#app .drop-zone .btn { margin: 10px 0 16px; }
/* Protocol overlay: image fixed, text scrolls natively */
/* Push popup below navbar at all widths */
#xftp-protocol { padding-top: 70px; }
#xftp-protocol .overlay-card {
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
@media (min-width: 768px) {
#xftp-protocol .overlay-card { max-height: min(660px, 100%); }
}
#xftp-protocol .overlay-card > h1 {
flex-shrink: 0;
padding-bottom: 16px;
margin-bottom: 0;
z-index: 1;
}
@media (min-width: 1024px) and (max-height: 750px) {
#xftp-protocol .overlay-card > h1 {
font-size: 22px;
}
}
/* .flex is the full-width scroll container */
#xftp-protocol .overlay-card > .flex {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #c5d3de transparent;
}
.dark #xftp-protocol .overlay-card > .flex {
scrollbar-color: #374151 transparent;
}
#xftp-protocol .overlay-card > .flex::-webkit-scrollbar { width: 6px; }
#xftp-protocol .overlay-card > .flex::-webkit-scrollbar-track { background: transparent; }
#xftp-protocol .overlay-card > .flex::-webkit-scrollbar-thumb { background: #c5d3de; border-radius: 3px; }
.dark #xftp-protocol .overlay-card > .flex::-webkit-scrollbar-thumb { background: #374151; }
/* Desktop: text stays left, image fixed on right */
@media (min-width: 1024px) and (min-height: 600px) {
#xftp-protocol .overlay-card > .flex > div:first-child {
max-width: 50%;
}
#xftp-protocol .overlay-card > .flex > div:last-child {
position: absolute;
right: 56px;
top: 0;
bottom: 0;
left: 55%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
#xftp-protocol .overlay-card > .flex > div:last-child > div {
height: 100%;
padding: 40px 0;
box-sizing: border-box;
}
#xftp-protocol .overlay-card > .flex > div:last-child img {
height: 100%;
}
}
#xftp-protocol .close-overlay-btn {
position: absolute;
top: 24px;
right: 24px;
cursor: pointer;
z-index: 3;
fill: #606C71;
}
.dark #xftp-protocol .close-overlay-btn { fill: #e5e7eb; }
/* Scroll fade: gradient over text area only, hidden at bottom */
#xftp-protocol .overlay-card::after {
content: '';
position: absolute;
bottom: 40px;
left: 24px;
right: 24px;
height: 48px;
background: linear-gradient(rgba(255,255,255,0), #fff);
pointer-events: none;
z-index: 2;
transition: opacity 0.2s;
}
.dark #xftp-protocol .overlay-card::after {
background: linear-gradient(rgba(7,28,70,0), #071C46);
}
@media (min-width: 640px) {
#xftp-protocol .overlay-card::after { bottom: 56px; left: 56px; right: 56px; }
}
@media (min-width: 1024px) and (min-height: 600px) {
#xftp-protocol .overlay-card::after { right: 55%; }
}
#xftp-protocol.scrolled-bottom .overlay-card::after { opacity: 0; }
#xftp-protocol .overlay-card.flex,
#xftp-protocol.flex .overlay-card { animation: overlaySlideIn 0.25s ease-out; }
@keyframes overlaySlideIn {
from { opacity: 0; transform: scale(0.95) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
#xftp-protocol.overlay-closing { animation: overlayFadeOut 0.2s ease-in forwards; }
#xftp-protocol.overlay-closing .overlay-card { animation: overlaySlideOut 0.2s ease-in forwards; }
@keyframes overlayFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes overlaySlideOut {
from { opacity: 1; transform: scale(1) translateY(0); }
to { opacity: 0; transform: scale(0.95) translateY(10px); }
}
.file-proto-link {
font-size: 1.25rem;
font-weight: 600;
background: linear-gradient(90deg, #001aa7 0%, #0095e7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-decoration: none;
}
.dark .file-proto-link {
background: linear-gradient(90deg, #019bfe 0%, #64fdff 58%, #c8feff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>
<section class="bg-secondary-bg-light dark:bg-primary-bg-dark py-10 mt-[66px] px-5">
<div class="container flex flex-col items-center mx-auto">
<noscript>
<p class="text-center text-grey-black dark:text-white py-10">
{{ "file-noscript" | i18n({}, lang) | safe }}
</p>
</noscript>
<h1 class="text-[38px] text-center font-bold text-active-blue mb-8">{{ 'file-title' | i18n({}, lang) | safe }}</h1>
<div id="app" data-xftp-app data-no-hashchange class="w-full">
<div class="card">
<div class="drop-zone">
<p>{{ 'file-drop-text' | i18n({}, lang) | safe }}</p>
<p class="hint">{{ 'file-drop-hint' | i18n({}, lang) | safe }}</p>
<span class="btn">{{ 'file-choose' | i18n({}, lang) | safe }}</span>
<p class="hint">{{ 'file-max-size' | i18n({}, lang) | safe }}</p>
</div>
</div>
</div>
<template id="dl-placeholder">
<div class="card">
<h1>{{ "file-title" | i18n({}, lang) | safe }}</h1>
<div class="stage">
<p>File available</p>
<button class="btn">{{ "file-download-btn" | i18n({}, lang) | safe }}</button>
<div class="security-note">
<p>{{ "file-dl-sec-1" | i18n({}, lang) | safe }}</p>
<p>{{ "file-sec-2" | i18n({}, lang) | safe }}</p>
<p>{{ "file-sec-3" | i18n({}, lang) | safe }}</p>
</div>
</div>
</div>
</template>
<script>if(location.hash.length>50){var a=document.getElementById("app"),t=document.getElementById("dl-placeholder");if(a&&t)a.replaceChildren(t.content.cloneNode(true))}</script>
<script type="module" src="/file-assets/index.js"></script>
<p class="text-base text-center text-grey-black dark:text-white mt-10 max-w-[600px]">{{ 'file-desc' | i18n({}, lang) | safe }}</p>
<a href="javascript:void(0)" data-show-overlay="xftp-protocol" class="open-overlay-btn file-proto-link mt-3 block text-center">{{ "file-learn-more" | i18n({}, lang) | safe }}</a>
</div>
</section>
<section class="bg-primary-bg-light dark:bg-secondary-bg-dark py-[90px] px-5">
<div class="container flex flex-col items-center mx-auto text-center">
<h2 class="text-[35px] leading-[45px] md:text-[45px] md:leading-[55px] text-grey-black dark:text-white font-bold mb-5">
{{ "file-cta-heading" | i18n({}, lang) | safe }}
</h2>
<p class="text-base text-grey-black dark:text-white mb-14 max-w-[600px]">
{{ "file-cta-subheading" | i18n({}, lang) | safe }}
</p>
<div class="flex items-center justify-center gap-4 flex-wrap">
<a href="https://apps.apple.com/us/app/simplex-chat/id1605771084" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apple_store.svg" /></a>
<a href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank"><img class="h-[40px] w-auto" src="/img/new/google_play.svg" /></a>
<a href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid"><img class="h-[40px] w-auto" src="/img/new/f_droid.svg" /></a>
<a href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="h-[40px] w-auto" src="/img/new/testflight.png" /></a>
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
</div>
</div>
</section>
{% for section in file_overlays.sections %}
{{ overlay(section, lang) }}
{% endfor %}
<script>
window.__XFTP_I18N__ = {
"title": {{ 'file-title' | i18n({}, lang) | dump | safe }},
"dropZone": {{ 'file-drop-text' | i18n({}, lang) | dump | safe }},
"dropZoneHint": {{ 'file-drop-hint' | i18n({}, lang) | dump | safe }},
"chooseFile": {{ 'file-choose' | i18n({}, lang) | dump | safe }},
"maxSizeHint": {{ 'file-max-size' | i18n({}, lang) | dump | safe }},
"encrypting": {{ 'file-encrypting' | i18n({}, lang) | dump | safe }},
"uploading": {{ 'file-uploading' | i18n({}, lang) | dump | safe }},
"cancel": {{ 'file-cancel' | i18n({}, lang) | dump | safe }},
"fileUploaded": {{ 'file-uploaded' | i18n({}, lang) | dump | safe }},
"copy": {{ 'file-copy' | i18n({}, lang) | dump | safe }},
"copied": {{ 'file-copied' | i18n({}, lang) | dump | safe }},
"share": {{ 'file-share' | i18n({}, lang) | dump | safe }},
"expiryHint": {{ 'file-expiry' | i18n({}, lang) | dump | safe }},
"securityNote1": {{ 'file-sec-1' | i18n({}, lang) | dump | safe }},
"securityNote2": {{ 'file-sec-2' | i18n({}, lang) | dump | safe }},
"securityNote3": {{ 'file-sec-3' | i18n({}, lang) | dump | safe }},
"retry": {{ 'file-retry' | i18n({}, lang) | dump | safe }},
"downloading": {{ 'file-downloading' | i18n({}, lang) | dump | safe }},
"decrypting": {{ 'file-decrypting' | i18n({}, lang) | dump | safe }},
"downloadComplete": {{ 'file-download-complete' | i18n({}, lang) | dump | safe }},
"download": {{ 'file-download-btn' | i18n({}, lang) | dump | safe }},
"fileTooLarge": {{ 'file-too-large' | i18n({}, lang) | dump | safe }},
"fileEmpty": {{ 'file-empty' | i18n({}, lang) | dump | safe }},
"invalidLink": {{ 'file-invalid-link' | i18n({}, lang) | dump | safe }},
"initError": {{ 'file-init-error' | i18n({}, lang) | dump | safe }},
"fileAvailable": {{ 'file-available' | i18n({}, lang) | dump | safe }},
"dlSecurityNote1": {{ 'file-dl-sec-1' | i18n({}, lang) | dump | safe }},
"dlSecurityNote2": {{ 'file-sec-2' | i18n({}, lang) | dump | safe }},
"dlSecurityNote3": {{ 'file-sec-3' | i18n({}, lang) | dump | safe }},
"workersRequired": {{ 'file-workers-required' | i18n({}, lang) | dump | safe }}
}
</script>
<script>
// Intercept close for #xftp-protocol to add fade-out animation
document.getElementById('xftp-protocol')?.addEventListener('click', function(e) {
if (e.target.closest('.close-overlay-btn') || (e.target.closest('.overlay') && !e.target.closest('.overlay-card'))) {
e.stopPropagation();
var el = document.getElementById('xftp-protocol');
el.classList.add('overlay-closing');
el.addEventListener('animationend', function handler() {
el.removeEventListener('animationend', handler);
el.classList.remove('flex', 'overlay-closing');
el.classList.add('hidden');
document.body.classList.remove('lock-scroll');
history.replaceState(null, null, ' ');
});
}
}, true);
// Hide scroll fade gradient when text bottom is visible (works with column-reverse)
var scrollArea = document.querySelector('#xftp-protocol .overlay-card > .flex');
if (scrollArea) {
var textCol = scrollArea.querySelector(':scope > div:first-child > div');
function checkScrollBottom() {
if (!textCol) return;
var scrollRect = scrollArea.getBoundingClientRect();
if (scrollRect.height === 0) return; // not laid out yet
var textBottom = textCol.getBoundingClientRect().bottom;
document.getElementById('xftp-protocol').classList.toggle('scrolled-bottom', textBottom <= scrollRect.bottom + 10);
}
scrollArea.addEventListener('scroll', checkScrollBottom);
new MutationObserver(function() {
if (!document.getElementById('xftp-protocol').classList.contains('hidden')) {
requestAnimationFrame(checkScrollBottom);
}
}).observe(document.getElementById('xftp-protocol'), { attributes: true, attributeFilter: ['class'] });
}
</script>

View File

@@ -0,0 +1,115 @@
<svg width="440" height="520" viewBox="-20 0 440 520" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Sender browser -->
<rect x="120" y="16" width="160" height="56" rx="10" stroke="#70F0F9" stroke-width="1.5"/>
<text x="200" y="40" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#70F0F9">Sender's browser</text>
<text x="200" y="56" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="rgba(112,240,249,0.7)">encrypts file</text>
<!-- Arrow down from sender to chunks -->
<line x1="200" y1="72" x2="200" y2="120" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
<!-- Chunks row -->
<rect x="112" y="120" width="176" height="40" rx="8" fill="none" stroke="#70F0F9" stroke-width="1" stroke-dasharray="4 3"/>
<text x="200" y="145" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#70F0F9">encrypted chunks</text>
<!-- Arrows from chunks to servers -->
<line x1="152" y1="160" x2="80" y2="220" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
<line x1="200" y1="160" x2="200" y2="220" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
<line x1="248" y1="160" x2="320" y2="220" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
<!-- Server 1 (SimpleX) -->
<rect x="20" y="220" width="120" height="56" rx="6" fill="none" stroke="#70F0F9" stroke-width="1.5"/>
<g transform="translate(28, 227)">
<rect width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<rect y="6" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<rect y="12" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<circle cx="11" cy="2" r="1" fill="#70F0F9"/>
<circle cx="11" cy="8" r="1" fill="#70F0F9"/>
<circle cx="11" cy="14" r="1" fill="#70F0F9"/>
</g>
<text x="80" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#70F0F9">SimpleX</text>
<text x="80" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="rgba(112,240,249,0.7)">XFTP server</text>
<!-- Server 2 (Flux) -->
<rect x="155" y="220" width="90" height="56" rx="6" fill="none" stroke="#70F0F9" stroke-width="1.5"/>
<g transform="translate(163, 227)">
<rect width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<rect y="6" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<rect y="12" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<circle cx="11" cy="2" r="1" fill="#70F0F9"/>
<circle cx="11" cy="8" r="1" fill="#70F0F9"/>
<circle cx="11" cy="14" r="1" fill="#70F0F9"/>
</g>
<text x="200" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#70F0F9">Flux</text>
<text x="200" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="rgba(112,240,249,0.7)">XFTP server</text>
<!-- Server 3 (SimpleX) -->
<rect x="260" y="220" width="120" height="56" rx="6" fill="none" stroke="#70F0F9" stroke-width="1.5"/>
<g transform="translate(268, 227)">
<rect width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<rect y="6" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<rect y="12" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
<circle cx="11" cy="2" r="1" fill="#70F0F9"/>
<circle cx="11" cy="8" r="1" fill="#70F0F9"/>
<circle cx="11" cy="14" r="1" fill="#70F0F9"/>
</g>
<text x="320" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#70F0F9">SimpleX</text>
<text x="320" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="rgba(112,240,249,0.7)">XFTP server</text>
<!-- Arrows from servers down -->
<line x1="80" y1="276" x2="152" y2="336" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
<line x1="200" y1="276" x2="200" y2="336" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
<line x1="320" y1="276" x2="248" y2="336" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
<!-- Re-encrypt label -->
<text x="330" y="310" text-anchor="start" font-family="system-ui, sans-serif" font-size="10" fill="rgba(112,240,249,0.7)">re-encrypted</text>
<text x="330" y="322" text-anchor="start" font-family="system-ui, sans-serif" font-size="10" fill="rgba(112,240,249,0.7)">per recipient</text>
<!-- Chunks row (download) -->
<rect x="112" y="336" width="176" height="40" rx="8" fill="none" stroke="#70F0F9" stroke-width="1" stroke-dasharray="4 3"/>
<text x="200" y="361" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#70F0F9">encrypted chunks</text>
<!-- Arrow down to recipient -->
<line x1="200" y1="376" x2="200" y2="424" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
<!-- Recipient browser -->
<rect x="120" y="424" width="160" height="56" rx="10" stroke="#70F0F9" stroke-width="1.5"/>
<text x="200" y="448" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#70F0F9">Recipient's browser</text>
<text x="200" y="464" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="rgba(112,240,249,0.7)">decrypts file</text>
<!-- Key path (dashed, side) -->
<path d="M120 44 L8 44 L8 452 L120 452" stroke="#70F0F9" stroke-width="1.5" stroke-dasharray="6 4" fill="none" marker-end="url(#arrowC)"/>
<text x="-6" y="240" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#70F0F9" transform="rotate(-90 -6 240)">key in URL fragment — never sent to server</text>
<!-- Closed padlock: encryption (between sender and chunks) -->
<g transform="translate(192, 88)">
<path d="M4,7 V4 C4,1.2 12,1.2 12,4 V7" stroke="#60a5fa" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<rect x="2" y="7" width="12" height="9" rx="2" fill="#60a5fa"/>
<circle cx="8" cy="12" r="1.2" fill="#0B2A59"/>
</g>
<!-- Open padlock: decryption (between chunks and recipient) -->
<g transform="translate(192, 392)">
<path d="M4,7 V4 C4,1.2 12,1.2 12,4 V2" stroke="#60a5fa" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<rect x="2" y="7" width="12" height="9" rx="2" fill="#60a5fa"/>
<circle cx="8" cy="12" r="1.2" fill="#0B2A59"/>
</g>
<!-- Key icon on dashed line -->
<g transform="translate(8, 410)">
<circle cx="0" cy="0" r="6" stroke="#FBBF24" stroke-width="2" fill="#FBBF24"/>
<circle cx="0" cy="0" r="2" fill="#0B2A59"/>
<line x1="6" y1="0" x2="16" y2="0" stroke="#FBBF24" stroke-width="2"/>
<line x1="14" y1="0" x2="14" y2="4" stroke="#FBBF24" stroke-width="2"/>
<line x1="11" y1="0" x2="11" y2="3.5" stroke="#FBBF24" stroke-width="2"/>
</g>
<!-- Annotation: no shared IDs -->
<text x="200" y="510" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="rgba(112,240,249,0.7)">Each chunk uses unique anonymous credentials — no shared identifiers</text>
<defs>
<marker id="arrowC" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#70F0F9"/>
</marker>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,130 @@
<svg width="440" height="520" viewBox="-20 0 440 520" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Sender browser -->
<rect x="120" y="16" width="160" height="56" rx="10" fill="url(#gBox)" stroke="#606C71" stroke-width="1.5"/>
<text x="200" y="40" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#fff">Sender's browser</text>
<text x="200" y="56" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="rgba(255,255,255,0.8)">encrypts file</text>
<!-- Arrow down from sender to chunks -->
<line x1="200" y1="72" x2="200" y2="120" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
<!-- Chunks row -->
<rect x="112" y="120" width="176" height="40" rx="8" fill="#f0f7ff" stroke="#0053D0" stroke-width="1" stroke-dasharray="4 3"/>
<text x="200" y="145" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#0053D0">encrypted chunks</text>
<!-- Arrows from chunks to routers -->
<line x1="152" y1="160" x2="80" y2="220" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
<line x1="200" y1="160" x2="200" y2="220" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
<line x1="248" y1="160" x2="320" y2="220" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
<!-- Router 1 (SimpleX) -->
<rect x="20" y="220" width="120" height="56" rx="6" fill="#f0f4f8" stroke="#606C71" stroke-width="1.5"/>
<g transform="translate(28, 227)">
<rect width="14" height="4" rx="1" fill="#606C71"/>
<rect y="6" width="14" height="4" rx="1" fill="#606C71"/>
<rect y="12" width="14" height="4" rx="1" fill="#606C71"/>
<circle cx="11" cy="2" r="1" fill="#53C1FF"/>
<circle cx="11" cy="8" r="1" fill="#53C1FF"/>
<circle cx="11" cy="14" r="1" fill="#53C1FF"/>
</g>
<text x="80" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#3F484B">SimpleX</text>
<text x="80" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="#606C71">XFTP router</text>
<!-- Router 2 (Flux) -->
<rect x="155" y="220" width="90" height="56" rx="6" fill="#f0f4f8" stroke="#606C71" stroke-width="1.5"/>
<g transform="translate(163, 227)">
<rect width="14" height="4" rx="1" fill="#606C71"/>
<rect y="6" width="14" height="4" rx="1" fill="#606C71"/>
<rect y="12" width="14" height="4" rx="1" fill="#606C71"/>
<circle cx="11" cy="2" r="1" fill="#53C1FF"/>
<circle cx="11" cy="8" r="1" fill="#53C1FF"/>
<circle cx="11" cy="14" r="1" fill="#53C1FF"/>
</g>
<text x="200" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#3F484B">Flux</text>
<text x="200" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="#606C71">XFTP router</text>
<!-- Router 3 (SimpleX) -->
<rect x="260" y="220" width="120" height="56" rx="6" fill="#f0f4f8" stroke="#606C71" stroke-width="1.5"/>
<g transform="translate(268, 227)">
<rect width="14" height="4" rx="1" fill="#606C71"/>
<rect y="6" width="14" height="4" rx="1" fill="#606C71"/>
<rect y="12" width="14" height="4" rx="1" fill="#606C71"/>
<circle cx="11" cy="2" r="1" fill="#53C1FF"/>
<circle cx="11" cy="8" r="1" fill="#53C1FF"/>
<circle cx="11" cy="14" r="1" fill="#53C1FF"/>
</g>
<text x="320" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#3F484B">SimpleX</text>
<text x="320" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="#606C71">XFTP router</text>
<!-- Arrows from routers down -->
<line x1="80" y1="276" x2="152" y2="336" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
<line x1="200" y1="276" x2="200" y2="336" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
<line x1="320" y1="276" x2="248" y2="336" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
<!-- Re-encrypt label -->
<text x="330" y="310" text-anchor="start" font-family="system-ui, sans-serif" font-size="10" fill="#606C71">re-encrypted</text>
<text x="330" y="322" text-anchor="start" font-family="system-ui, sans-serif" font-size="10" fill="#606C71">per recipient</text>
<!-- Chunks row (download) -->
<rect x="112" y="336" width="176" height="40" rx="8" fill="#f0f7ff" stroke="#0053D0" stroke-width="1" stroke-dasharray="4 3"/>
<text x="200" y="361" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#0053D0">encrypted chunks</text>
<!-- Arrow down to recipient -->
<line x1="200" y1="376" x2="200" y2="424" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
<!-- Recipient browser -->
<rect x="120" y="424" width="160" height="56" rx="10" fill="url(#gBox)" stroke="#606C71" stroke-width="1.5"/>
<text x="200" y="448" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#fff">Recipient's browser</text>
<text x="200" y="464" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="rgba(255,255,255,0.8)">decrypts file</text>
<!-- Key path (dashed, side) -->
<path d="M120 44 L8 44 L8 452 L120 452" stroke="#0053D0" stroke-width="1.5" stroke-dasharray="6 4" fill="none" marker-end="url(#arrowB)"/>
<text x="-6" y="240" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#0053D0" transform="rotate(-90 -6 240)">key in URL fragment - never sent to page server or data router</text>
<!-- Closed padlock: encryption (between sender and chunks) -->
<g transform="translate(192, 88)">
<path d="M4,7 V4 C4,1.2 12,1.2 12,4 V7" stroke="#0053D0" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<rect x="2" y="7" width="12" height="9" rx="2" fill="#0053D0"/>
<circle cx="8" cy="12" r="1.2" fill="#fff"/>
</g>
<!-- Open padlock: decryption (between chunks and recipient) -->
<g transform="translate(192, 392)">
<path d="M4,7 V4 C4,1.2 12,1.2 12,4 V2" stroke="#0053D0" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<rect x="2" y="7" width="12" height="9" rx="2" fill="#0053D0"/>
<circle cx="8" cy="12" r="1.2" fill="#fff"/>
</g>
<!-- Key icon on dashed line -->
<g transform="translate(8, 410)">
<circle cx="0" cy="0" r="6" stroke="#D97706" stroke-width="2" fill="#D97706"/>
<circle cx="0" cy="0" r="2" fill="#fff"/>
<line x1="6" y1="0" x2="16" y2="0" stroke="#D97706" stroke-width="2"/>
<line x1="14" y1="0" x2="14" y2="4" stroke="#D97706" stroke-width="2"/>
<line x1="11" y1="0" x2="11" y2="3.5" stroke="#D97706" stroke-width="2"/>
</g>
<!-- Annotation: no shared IDs -->
<text x="200" y="510" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#606C71">Each file fragment uses unique anonymous credentials - no shared identifiers</text>
<defs>
<linearGradient id="gBox" x1="120" y1="16" x2="280" y2="72" gradientUnits="userSpaceOnUse">
<stop stop-color="#0053D0"/>
<stop offset="1" stop-color="#53C1FF"/>
</linearGradient>
<linearGradient id="gSrv1" x1="20" y1="220" x2="140" y2="276" gradientUnits="userSpaceOnUse">
<stop stop-color="#0053D0"/>
<stop offset="1" stop-color="#53C1FF"/>
</linearGradient>
<linearGradient id="gSrv2" x1="155" y1="220" x2="245" y2="276" gradientUnits="userSpaceOnUse">
<stop stop-color="#0053D0"/>
<stop offset="1" stop-color="#53C1FF"/>
</linearGradient>
<marker id="arrowG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#606C71"/>
</marker>
<marker id="arrowB" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0053D0"/>
</marker>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -130,7 +130,7 @@ if (isMobile.iOS) {
}
function clickHandler(e) {
if (e.target.closest('.card')) {
if (e.target.closest('.card') && !e.target.closest('[data-xftp-app]')) {
e.target.closest('.card').classList.toggle('card-active');
e.target.closest('.card').classList.toggle('no-hover');
}
@@ -182,7 +182,7 @@ function openOverlay() {
if (hash) {
const id = hash.split('#')[1];
const el = document.getElementById(id)
if (el.classList.contains('overlay')) {
if (el && el.classList.contains('overlay')) {
const scrollTo = el.getAttribute('data-scroll-to')
if (scrollTo) {
const scrollToEl = document.getElementById(scrollTo)

View File

@@ -1,6 +1,6 @@
module.exports = {
darkMode : 'class',
content: ["./src/**/*.{html,js,njk}"],
content: ["./src/**/*.{html,js,njk}", "!./src/file-assets/**"],
theme: {
extend: {
backgroundImage: {

View File

@@ -1,6 +1,8 @@
#!/bin/bash
set -e
# Eleventy OOMs with default 2GB V8 heap when building 280+ pages across 23 languages
export NODE_OPTIONS=--max-old-space-size=4096
cp -R docs website/src
rm -rf website/src/docs/contributing
@@ -29,6 +31,10 @@ npm install
cp node_modules/lottie-web/build/player/lottie.min.js src/js
cp node_modules/ethers/dist/ethers.umd.min.js src/js
cp node_modules/ethers/dist/ethers.umd.js.map src/js
mkdir -p src/file-assets
cp node_modules/@simplex-chat/xftp-web/dist-web/assets/index.js src/file-assets/
cp node_modules/@simplex-chat/xftp-web/dist-web/assets/index.css src/file-assets/
cp node_modules/@simplex-chat/xftp-web/dist-web/assets/crypto.worker.js src/file-assets/
node merge_translations.js
node customize_docs_frontmatter.js
@@ -42,6 +48,7 @@ for lang in "${langs[@]}"; do
cp src/invitation.html src/$lang
cp src/fdroid.html src/$lang
cp src/why.html src/$lang
cp src/file.html src/$lang
echo "{\"lang\":\"$lang\"}" > src/$lang/$lang.json
echo "done $lang copying"
done