Telegram's rich text editor — for everyone, without Premium.
Because bots should not have more rights than humans.
🇮🇷 فارسی • Quick start • Checklists • The window • Features • Architecture • Security • Troubleshooting • Development
In June 2026 Telegram shipped a rich text editor — and locked it behind Premium. The very same formatting is available to every bot, for free, through Bot API 10.1.
| Telegram Premium | A bot + this app | |
|---|---|---|
| Headings, quotes, tables, code blocks | 💎 Premium | ✅ Free |
| Spoilers, marked text, sub/superscript | 💎 Premium | ✅ Free |
| Checklists, collapsibles, formulas | 💎 Premium | ✅ Free |
| Monthly cost | 💸 | 0 |
No coding, no server, no subscription. Point the app at a bot token and a chat, and write.
1 — Get a bot. Message @BotFather, send /newbot, copy the token.
Add the bot to your channel or group as an administrator.
2 — Install. Grab a package from the latest release:
| Platform | File | Notes |
|---|---|---|
| Windows 10/11 | TelegramFreeRich-Setup-*.exe |
Installs for the current user — no admin rights |
| Windows 10/11 | TelegramFreeRich-*-portable.exe |
Runs as-is, nothing to install |
| Linux | TelegramFreeRich-*.AppImage |
chmod +x it and run |
Or start from source:
git clone https://github.com/Aporis3674/TelegramFreeRich.git
cd TelegramFreeRich
npm install
npm run dev3 — Configure. Click the ⚙ gear in the title bar, paste the token and the chat ID
(@channel or a numeric ID), press Test connection, then Save.
Write your message and press Ctrl+Enter. That's it.
Telegram is blocked in some countries, and people reach it through v2rayN, Nekoray or a similar
client. Those clients offer “Set system proxy”, which configures Windows — and therefore
Chromium — but not Node. Any Electron app that sends with Node's https module ignores it
and times out.
This app sends every Telegram request through Chromium's network stack (net.request on the
default session), so the system proxy is honoured, PAC scripts included.
Settings → Connection:
| Mode | When to use it |
|---|---|
| System proxy (default) | Your VPN client has “Set system proxy” enabled — nothing else to do |
| Manual proxy | The client only exposes a port. One click fills in v2rayN's defaults: SOCKS5 127.0.0.1:10808 or HTTP 127.0.0.1:10809 |
| No proxy | Direct connection |
Check connection to Telegram reports which proxy Chromium resolved and whether
api.telegram.org answered, so a blocked network is never mistaken for a bad token. Manual
proxies may carry a username and password; the password is encrypted next to the bot token and
never reaches the window you type in.
A checklist travels inside the message. Rich HTML has its own task-list form —
<ul><li><input type="checkbox" checked>done</li></ul> — so Telegram draws real checkboxes, in a
channel, a group or a private chat alike, with the ticks you set in the editor preserved. Nothing
extra to configure; write a checklist and send it.
Telegram also has a second, different thing: an interactive checklist, sent with the
sendChecklist method, whose boxes readers can tick themselves. That one is restricted to a bot
acting on behalf of a connected business account, in a private chat — and it comes with its own
limits, 30 tasks and 100 characters each, and no way to send a task already ticked
(markChecklistTasksAsDone does that afterwards).
The app picks between them for you:
| Settings ▸ Business connection ID | Chat | What gets sent |
|---|---|---|
| empty (the normal case) | anything, channels included | checkboxes inside the message |
| set | private (numeric ID) | an interactive checklist — the app asks for its title |
| set | channel or group | checkboxes inside the message, with a note saying why |
A faithful rebuild of Telegram Desktop's composer: frameless, dark, hairline-outlined pill buttons and one salmon send button.
┌ ⚙ ▣ ¶ ──────────────────────────────────────────── ─ □ ✕ ┐
│ ( ↶ )( ↷ ) ( Aa )( B )( ☰ )( ▦ )( 🔗 )( 🖼 )( Σ ) │
│ │
│ Message │ Live preview │
│ │ │
│ ( ✦ ) 342 / 32,768 ( 🗑 ) ( ➤ ) │
└──────────────────────────────────────────────────────────────┘
| Button | Click it and you get |
|---|---|
| ↶ ↷ | Undo / redo — the full ProseMirror history |
| Aa | Formatting → Heading (opens H1–H6), Text, Quote, Pull quote, Attribution (the <cite> a quote may name), Code block, Footer, Divider |
| B | Text style → Bold, Italic, Underline, Strikethrough, Spoiler, Subscript, Superscript, Marked |
| ☰ | Insert list → Ordered list, Bullet list, Checklist (see Checklists), Details |
| ▦ | Insert table → an editable 3×3 table; a floating bar adds and removes rows and columns, and column widths drag on the borders |
| 🔗 | Insert link → a Create link panel with Text and URL fields |
| 🖼 | Insert media → Photo or video (two or more become a slideshow), Audio file, Location, Caption… (the <figcaption> under a media item) |
| Σ | Insert formula → asks for the formula, drops in a math block |
| ✦ | The insert palette — search every block and format by name |
| 🗑 ➤ | Clear the message · Send it (right-click to pick rich message, draft or edit) |
The title bar holds the settings gear with a connection dot, the live-preview toggle and the right-to-left switch for the message you are writing.
No emoji button. Custom emoji are a Premium-only entity that bots cannot send through the rich-message API. Plain Unicode emoji typed from your keyboard travel as ordinary text.
| Text | Bold · Italic · Underline · Strikethrough · Spoiler · Marked · Subscript · Superscript · Monospace · Inline formula · Links |
| Blocks | Heading H1–H6 · Paragraph · Quote (with attribution) · Pull quote (with attribution) · Code block with language · Divider · Collapsible · Footer · Formula block |
| Lists | Ordered · Bulleted · Checklist (see Checklists) |
| Tables | Header row, editable cells, drag-to-resize columns |
| Media | Photo · Video · Audio · Slideshow (‹ › carousel) · Collage · Location — each with an optional caption |
And while you write:
- Live preview — a real Telegram bubble, rendered from the same document you are editing
- Markdown shortcuts —
##,>,-,1.,```,**bold**,`code` - Insert palette — one search box over every block and format
- Dark and light themes, English and Persian UI, full right-to-left support
- Character counter against Telegram's 32,768 limit
Right-click the send button:
| Mode | What it does |
|---|---|
| Rich message | sendRichMessage — the normal path |
| Draft | sendRichMessageDraft — a 30-second preview, private chats only |
| Edit | editMessageText — rewrites an existing message (ID comes from Settings) |
| Ctrl+Enter Send | Ctrl+K Link | Ctrl+P Preview | Ctrl+, Settings |
| Ctrl+B Bold | Ctrl+I Italic | Ctrl+U Underline | Ctrl+E Monospace |
| Ctrl+Shift+X Strikethrough | Ctrl+Shift+P Spoiler | Ctrl+Z Undo | Ctrl+Shift+Z Redo |
sendRichMessage takes rich_message: { html | markdown } — exactly one of the two. The
RichBlock array is the receiving shape: what a bot reads back from Message.rich_message,
not what it sends. So the app serializes the editor into Telegram's Rich HTML dialect:
TipTap (ProseMirror) document
│
▼
HTML serializer src/shared/html-serializer.js
├─ keeps <b> <i> <u> <s> <code> <mark> <sub> <sup> <a>
│ <p> <h1>…<h6> <ul> <ol> <li> <pre> <blockquote> <footer> <hr/>
│ <table> <details> <img> <video> <audio>
├─ maps spoiler → <tg-spoiler> pull quote → <aside>
│ math → <tg-math> / <tg-math-block>
│ gallery → <tg-collage> / <tg-slideshow>
│ location → <tg-map lat long zoom/>
└─ drops every tag and URL scheme the API does not document
│
▼
rich_message: { html, is_rtl?, skip_entity_detection? }
│
├──► sendRichMessage rich messages
├──► sendRichMessageDraft 30-second drafts (private chats, needs draft_id)
├──► editMessageText rewrite an existing message
└──► sendChecklist interactive checklist, business only
Before sending, the serializer counts what it produced and checks the documented limits: 32,768 characters, 500 blocks, 50 media files, 20 table columns.
The live preview keeps its own path — block-parser.js and inline-parser.js build the
RichBlock / RichText shapes Telegram sends back, which is what the bubble renders.
Editor → Rich HTML
| In the editor | On the wire |
|---|---|
| heading H1–H6 | <h1>…<h6> |
| paragraph | <p> |
| quote | <blockquote> — plus <cite> when it names a source |
| pull quote | <aside> — plus <cite> when it names a source |
| code block | <pre><code class="language-…"> |
| divider | <hr/> |
| bullet / ordered list | <ul> / <ol start="…"> |
| table | <table bordered> with <th> / <td> |
| collapsible | <details open><summary> |
| footer | <footer> |
| photo / video / audio | <img src> / <video src> / <audio src> — plus <figcaption> when captioned |
| slideshow / collage | <tg-slideshow> / <tg-collage> |
| location | <tg-map lat long zoom/> |
| formula block / inline | <tg-math-block> / <tg-math> |
| checklist | <ul><li><input type="checkbox">… — or lifted out for sendChecklist |
Inline marks
| Mark | On the wire | Telegram entity |
|---|---|---|
| bold / italic / underline / strikethrough | <b> <i> <u> <s> |
RichTextBold, RichTextItalic, RichTextUnderline, RichTextStrikethrough |
| spoiler | <tg-spoiler> |
RichTextSpoiler |
| marked | <mark> |
RichTextMarked |
| monospace | <code> |
RichTextCode |
| subscript / superscript | <sub> / <sup> |
RichTextSubscript / RichTextSuperscript |
| link | <a href> |
RichTextUrl |
| inline formula | <tg-math> |
RichTextMathematicalExpression |
Links keep only http, https, mailto, tel, tg and in-document #anchor targets; media
accepts http(s) only, exactly as the API requires.
The bot token is the one secret in this app, and it never reaches the window you type in.
Renderer (React) Main process (Electron)
───────────────── ───────────────────────
✗ never holds the token ✓ token encrypted with safeStorage
✗ never calls the network ──IPC──► ✓ every Telegram request starts here
✓ knows only "a token exists" ✓ validates method, chat ID, language
✓ 30 s timeout · 1 MB response cap
| Layer | Protection |
|---|---|
| Token storage | safeStorage — OS keychain encryption, on disk as settings.enc; the proxy password sits beside it |
| IPC bridge | contextBridge with contextIsolation — no Node APIs in the renderer |
| Input validation | Allowlists for API methods, chat IDs, languages and the business connection ID |
| HTTP | 30-second timeout, 1 MB response limit |
| URLs | javascript:, data: and vbscript: schemes are refused |
| Rendering | The preview builds React elements — never innerHTML |
| CSP | Content-Security-Policy meta tag in index.html |
| What you see | What it means |
|---|---|
Network error: net::ERR_TUNNEL_CONNECTION_FAILED |
The proxy is set but not answering. Start your VPN client, or check the host and port in Settings ▸ Connection |
Network error: net::ERR_CERT_AUTHORITY_INVALID |
A proxy is terminating TLS. Install its certificate, or pick a different proxy |
| Check connection works but sending fails | Read the message after Network error:. Anything beginning Bad Request came from Telegram and is about the message itself, not the network |
Bad Request: chat not found |
The bot is not in that chat, or the chat ID is wrong. A channel needs @username or its -100… ID, and the bot must be an administrator |
Bad Request: not enough rights |
Give the bot permission to post in the channel or group |
| Windows: Failed to uninstall old application files | A leftover TelegramFreeRich.exe is holding the files. Since 5.1.1 the installer closes it itself; on an older version, end the process in Task Manager first, or use the portable build |
| The window is blank | Delete settings.enc from %APPDATA%/TelegramFreeRich (or ~/.config/TelegramFreeRich) and start again — you will have to re-enter the token |
npm run dev # Vite + Electron with hot reload
npm test # 265 unit tests (Vitest)
npm run lint # ESLint, zero warnings
npm run format # Prettier
npm run build # Windows: dist/TelegramFreeRich-Setup-5.4.0.exe + …-portable.exe
npm run build:linux # Linux: dist/TelegramFreeRich-5.4.0.AppImageProject layout
installer/installer.nsh NSIS customisation — closes the app before upgrading
src/
├── main/ Electron main process
│ ├── main.js window, IPC handlers, encrypted settings
│ ├── preload.js contextBridge — the only renderer surface
│ ├── net/proxy.js system / manual / direct proxy resolution
│ ├── net/request.js requests over Chromium's stack, proxy-aware errors
│ └── security/validation.js method / chat ID / language / business ID allowlists
│
├── renderer/ React UI
│ ├── App.jsx shell: state, send logic, shortcuts
│ ├── components/
│ │ ├── TitleBar.jsx frameless window chrome
│ │ ├── Toolbar.jsx the seven-button strip + menus
│ │ ├── ActionMenu.jsx menu renderer, two levels deep
│ │ ├── Popover.jsx anchored floating panel
│ │ ├── TableBubble.jsx floating table row/column controls
│ │ ├── InsertPalette.jsx searchable palette
│ │ ├── BottomBar.jsx palette · counter · clear · send
│ │ ├── Preview.jsx live Telegram bubble
│ │ ├── Settings.jsx token, chat, business connection, proxy, theme
│ │ ├── Dialog.jsx promise-based prompt / confirm / link panel
│ │ ├── Toast.jsx notifications
│ │ ├── Icons.jsx the SVG set
│ │ ├── useTfrEditor.js TipTap setup
│ │ └── extensions.js custom nodes and marks
│ ├── lib/editor-actions.js one registry behind every menu
│ ├── i18n/ en · fa · provider
│ └── styles/ theme · app · toolbar · menu · editor · preview
│
└── shared/ used by both processes
├── block-types.js BlockType / InlineType enums
├── block-parser.js DOM → Block State
├── inline-parser.js inline DOM → styled segments
├── html-serializer.js editor DOM → Rich HTML + request bodies
├── block-manager.js CRUD + undo/redo
├── constants.js limits and defaults
└── utils.js sanitizeUrl, validation, helpers
tests/unit/ 265 tests — HTML serializer, proxy, packaging, docs, parsers,
i18n parity, stylesheets, validation
Adding a command means adding one entry to src/renderer/lib/editor-actions.js; the menus,
the palette and the tests all read from that registry.
Custom TipTap extensions
| Extension | Kind | Renders as |
|---|---|---|
| Spoiler | mark | <span data-spoiler> |
| InlineMath | mark | <span data-inline-math> |
| PullQuote | node | <blockquote data-pullquote> |
| Details | node | <details> with an editable body |
| Footer | node | <footer> |
| MathBlock | node | div.tg-math |
| MediaBlock | node | <video> / <audio> |
| GalleryBlock | node | div.tg-gallery — slideshow or collage |
| MapBlock | node | div.tg-map |
| Layer | Choice |
|---|---|
| Desktop shell | Electron 35 — frameless window, safeStorage, contextBridge |
| UI | React 19 + Vite 6 |
| Editor | TipTap 2 (ProseMirror) |
| Data model | Editor DOM → Telegram Rich HTML (rich_message.html) |
| Tests | Vitest 2 + jsdom |
| Packaging | electron-builder — NSIS installer, Windows portable, AppImage |
| CI | GitHub Actions — lint, test, build |
Changelog • Design spec • Bot API docs
MIT © Aporis3674
The best things in life are free. The second-best are also free, if you use a bot.
