< Back to list of posts

Building a Native Telegram Chat Viewer for macOS

I recently built a desktop application to solve a problem I kept running into: browsing through exported Telegram chats. If you've ever exported a Telegram conversation, you know you get a folder full of HTML files that aren't particularly easy to navigate or search through. I wanted something better, a native app that felt at home on macOS and could actually handle my large chat exports without grinding to a halt.

The Problem Space

Telegram's export feature is fantastic for backing up your conversations, but the output isn't exactly user-friendly. You get:

  • Multiple HTML files (messages.html, messages2.html, etc.) that split your conversation across files
  • A folder structure with photos, videos, and other attachments
  • Basic styling that works in a browser but lacks search, filtering, or any modern conveniences

For small exports, opening these in a browser works fine. But when you're dealing with years of messages (think 10,000+ in a single chat), browsers start to struggle. I needed something purpose-built.

Why a Desktop App?

I could have made this a web app (and actually started there), but I kept hitting limitations. Desktop apps have access to the file system in ways that web apps can't. Native file pickers, reading entire folders at once, and better performance with large datasets all pushed me toward going native.

The question was: which framework?

Choosing Tauri Over Electron

Most developers building desktop apps with web technologies reach for Electron. It's mature, well-documented, and powers apps like VS Code and Slack. But Electron apps are notoriously heavy. Shipping an entire Chromium browser with every app means bloated bundle sizes and hefty memory usage.

Enter Tauri.

Tauri is a newer framework that takes a different approach. Instead of bundling a browser, it uses the operating system's native WebView (on macOS, that's WKWebView). The backend is written in Rust rather than Node.js, which gives you:

  • Tiny bundle sizes - My app builds to under 10MB compared to 100MB+ with Electron
  • Better performance - Rust is fast, and the native WebView is optimized by Apple
  • Lower memory usage - No bundled browser means less RAM consumption
  • Native feel - Apps integrate seamlessly with macOS conventions

The trade-off? Less mature ecosystem and a steeper learning curve if you're not familiar with Rust. But for a macOS-only app, the benefits far outweighed the costs.

The Tech Stack

I landed on a stack that balanced modern developer experience with performance:

Frontend:

Backend:

  • Tauri 2.0as the desktop framework
  • Rust for the backend (though I kept it minimal)
  • Tauri plugins for file system access and native dialogs

Build Tools:

  • Vite for lightning-fast development and builds
  • TypeScript in strict mode to catch errors early

The Architecture

The app has a straightforward architecture:

  1. File Loading: Users can select either a folder (containing all the export files) or individual HTML files through native macOS dialogs
  2. Parsing: Cheerio parses the HTML to extract messages, timestamps, sender names, links, and media attachments
  3. State Management: React hooks manage the application state, so there's no need for Redux or MobX here
  4. Rendering: Messages display in either a compact list view or a more detailed card view
  5. Search: Real-time filtering across all message content, sender names, and links

The parsing logic was particularly interesting to implement. Telegram's HTML structure looks like this:

<div class="message default clearfix" id="message228402"> <div class="body"> <div class="date" title="23.05.2023 05:54:24 UTC-05:00">05:54</div> <div class="from_name">deviant cat</div> <div class="text"> <a href="https://example.com">https://example.com</a> </div> </div> </div>

I built a parser that extracts:

  • Message IDs for unique identification
  • Sender names (with handling for "joined" messages where the sender repeats)
  • Full timestamps from the title attribute
  • Message text content
  • All links in the message
  • Media attachments with type detection (PDFs, images, zips, etc.)

Features that made a difference

Two View Modes

I implemented both list and card views because different situations call for different browsing patterns:

List View is compact and fast, perfect for skimming through thousands of messages. It uses react-window for virtual scrolling, meaning only visible messages render. This keeps the app responsive even with 10,000+ messages loaded.

Card View gives you more breathing room. Messages display in a responsive grid (1-3 columns depending on window width) with full text content and link previews. Great for reading through conversations more deliberately.

Smart link previews

One pattern I noticed in my exports: lots of messages that were just a single link. Rather than displaying these as plain text, I built rich link preview cards that show:

  • The domain and site name
  • An extracted or generated title
  • The full URL
  • If I can fetch metadata (via Open Graph tags), the description and image preview

These previews are lazy-loaded. They only fetch metadata when scrolling near them, keeping initial load times fast.

Real-time search

The search functionality might be my favorite feature. It's:

  • Fast - Debounced to 150ms with memoized filtering
  • Comprehensive - Searches message text, sender names, URLs, and media filenames
  • Live - Shows "X of Y messages" count that updates as you type
  • Keyboard-friendly - ⌘F focuses the search, Esc clears it

Native macOS integration

To make this feel like a real Mac app, not just a web page in a window, I implemented:

  • Native menu bar with File and View menus
  • Keyboard shortcuts that Mac users expect (⌘O to open, ⌘1/⌘2 to switch views)
  • Native file and folder pickers
  • Opening links in the user's default browser
  • System-aware dark mode that respects macOS appearance settings

Dark mode and media previews

Rather than ship without polish, I added two features that made a real difference:

Dark Mode: The app now includes a complete dark theme that adapts all UI elements. I didn't want a half-baked implementation where some components still burned your retinas at night. Every card, button, search bar, and text element has been carefully styled for both light and dark modes. Users can toggle between themes with a button in the header, and the preference persists across sessions.

Media Previews: Messages with image attachments now display actual thumbnails rather than just filenames. When someone shares a photo in the chat, you see it inline with the message. The app locates the image files in the export folder and renders them directly, making photo-heavy conversations much easier to browse. I implemented lazy loading here too, so images only load as you scroll near them. That keeps performance smooth even with hundreds of photos in a chat.

Telegram Chat Viewer Screenshot showing the Telegram Viewer App

The app icon

I designed the app icon using ChatGPT. I described what I wanted and iterated through a few variations until I got something I liked. Small detail, but a polished icon makes the app feel more real when it sits in your dock next to everything else.

App logo Screenshot showing the Telegram Viewer App Logo

Performance optimizations

Large chat exports were my benchmark for performance. Here's what I did:

Memoization Everywhere: React's useMemo and useCallback hooks prevent unnecessary recalculations. Filtering 10,000 messages is real work, and there's no need to redo it on every render when only the search term changed.

Component Memoization: Message cards are wrapped in React.memo with custom comparison functions. If a message's data hasn't changed, don't re-render it.

Virtual Scrolling: The list view only renders visible messages plus a small buffer. Scrolling through 50,000 messages feels just as smooth as scrolling through 50.

Lazy Loading: Link preview metadata only fetches when the preview scrolls near the viewport, using Intersection Observer.

Sequential Parsing: Rather than trying to parse all files at once, the app parses them sequentially with a loading indicator. This keeps the UI responsive and gives users feedback.

What I learned

Rust isn't as scary as I thought

I was hesitant about writing Rust, but Tauri abstracts most of it away. My main.rs file is barely 15 lines, mostly just menu bar configuration. The Tauri plugins handle all the heavy lifting for file system access.

TypeScript pays off even more in desktop apps

Type safety became more valuable than usual here. The back-and-forth between Tauri commands, React state, and file system operations created a lot of places where types could mismatch. TypeScript caught those at compile time.

Users notice native details

Beta testers immediately noticed things like keyboard shortcuts, native file pickers, and menu bar integration. These details matter for desktop apps in a way they don't for web apps.

Performance is about perception

Getting 10,000 messages to load in under 5 seconds wasn't enough. Users also needed to see progress. Loading indicators, skeleton screens, and progressive rendering made the app feel fast even during heavy operations.

The build process

The development workflow is solid:

npm run tauri:dev

This starts the Vite dev server and launches the app with hot module reloading. Changes to React components appear instantly. Changes to Rust code trigger a recompile (which is slower, but I rarely touched the Rust side).

Building for production:

npm run tauri:build

This creates a .dmg installer ready for distribution. The entire build, including compiling the Rust backend and bundling the frontend, takes about 2 minutes on my machine.

Screen recording of the app in use

What's next

This is version 1.0 with dark mode and media previews. Some things I'd like to add:

  • Date range filters - Search by time period
  • Export functionality - Save filtered results to CSV or JSON
  • Multi-chat support - Load and switch between multiple chat exports
  • Message threading - Display replies and quoted messages
  • Advanced filters - Filter by media type, sender, or date
  • PDF export - Generate formatted PDFs of conversations

Try it yourself

The app is open source and available on my GitHub. If you have Telegram chat exports sitting around and want a better way to browse them, give it a try. You'll need:

  • Node.js (v18+)
  • Rust (latest stable)
  • Xcode Command Line Tools

Then just:

git clone [your-repo-url] cd telegram-chat-viewer npm install npm run tauri:dev

Final thoughts

Building this taught me that desktop app development doesn't have to mean Electron. Tauri is a solid alternative, especially for single-platform apps where you can lean on the native WebView.

With React, TypeScript, and Tailwind, I built a UI in days that would have taken weeks in native Swift. But unlike a web app, this feels fast, lightweight, and actually native.

If you're thinking about building a desktop app, give Tauri a look.

Buy Me A Coffee