Build a WhatsApp Chat Viewer with React

Parse WhatsApp exported chat files and render them in a dark-mode UI.

Introduction

30 Second Summary

WhatsApp chats capture years of conversations with friends, family, and colleagues. When you export a chat as a plain text file, though, it's just a wall of timestamps and names that's nearly impossible to read back.

In this project, you will build a browser-based WhatsApp Chat Viewer using React. You'll upload a real exported chat file and see it rendered as a polished WhatsApp Web dark-mode conversation, complete with colored sender names, message bubbles, timestamps, and date separators.

What You'll Build

Picture uploading a .txt file from your phone, picking your name from a dropdown, and watching the entire conversation snap into place with your messages on the right in green bubbles and everyone else's on the left in gray.

By the end of this project, you'll have:

  • A working file upload that reads any WhatsApp exported .txt file and renders the full conversation instantly with proper message bubbles, sender names, and timestamps.
  • A sidebar panel displaying chat statistics (total messages, participants, date range) and a bubble-side selector that lets you pick which participant is "you" and watch all messages re-align in real time.
  • A pixel-accurate WhatsApp Web dark-mode design with green outgoing bubbles, dark gray incoming bubbles, colored sender names, and date separator pills dividing the conversation by day.
  • Secret Mission: Add a search bar and participant filter to find specific messages within the chat.

Are there any prerequisites?

A basic understanding of JavaScript and React concepts is helpful, but no practical coding experience is required. This project walks you through every line of code.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your React Project with Vite

To build the WhatsApp Chat Viewer, you first need a working React development environment. Without one, you have no way to write components, manage state, or see live changes in the browser.

Vite is a modern build tool that scaffolds a React project in seconds and gives you a live-reloading dev server. Every time you save a file, Vite updates the browser instantly.

In this step, get ready to:
  • Scaffold a new React project using Vite.
  • Install the project dependencies.
  • Start the development server and confirm it loads in the browser.
Scaffold a new React project

Vite's scaffold command creates all the starter files you need: a project folder, configuration, and a ready-to-run React app.

  • In VS Code, open a new terminal by clicking Terminal in the top menu bar.
  • Click New Terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your React project by running this command:
npm create vite@latest whatsapp-chat-viewer -- --template react

What does this command do?

  • npm create vite@latest downloads and runs Vite's project generator.
  • whatsapp-chat-viewer is the name of your new project folder.
  • -- --template react tells Vite to use the React starter template (JavaScript, not TypeScript).

npm asks to install a package

If npm prompts you to install the create-vite package, press Enter to confirm. This is normal the first time you run the command.

You should see output ending with a message that your project has been scaffolded, along with suggested next steps.

Seeing an error about Node.js version?

Vite requires Node.js version 20.19+ or 22.12+. Run node --version to check yours.

If your version is too old, update Node.js from the official website before retrying. Help me fix my Node.js version for Vite

Open the project and install dependencies

Now that the project folder exists on your Desktop, open it in VS Code so you can see all the files Vite generated.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the whatsapp-chat-viewer folder, and click Open.

Trust the folder if prompted

VS Code may ask if you trust the authors of the folder. Click Yes, I trust the authors to continue.

Your Explorer sidebar now shows the project files Vite created, including src/, index.html, and package.json.

  • Open a new terminal by clicking Terminal in the top menu bar, then New Terminal.
  • Install the project dependencies by running:
npm install

You should see output showing packages being added, ending with a summary of installed packages.

Seeing permission or network errors?

Make sure your terminal is inside the whatsapp-chat-viewer folder (check the prompt path). If you see network errors, check your internet connection and retry.

Help me fix npm install errors in my Vite project

Start the development server

Everything is installed. Time to see your app running in the browser.

  • Start the development server by running:
npm run dev

Before you check the browser, what do you think you will see at the URL Vite provides?

  • Open your browser and navigate to http://localhost:5173.

You should see the Vite + React starter page with the Vite and React logos and a counter button.

What is localhost:5173?

localhost means your own computer. 5173 is the port number Vite uses by default. Together, this URL points to the dev server running on your machine.

The server watches for file changes and refreshes the browser automatically. Leave it running in this terminal.

Browser shows nothing or a connection error?

Check that the terminal still shows the server running (you should see a Local: URL in the output). If you accidentally closed it, run npm run dev again.

Make sure you are visiting http://localhost:5173 exactly (not https).

Help me get the Vite dev server running

Your React development environment is ready and running. Next up, you will build the file upload component and parse a real WhatsApp chat export into structured data.

Build the File Upload and Chat Parser

Your React project is running and you can see the Vite starter page in your browser. Now you need to give it something useful to do.

WhatsApp lets you export chat histories as plain .txt files. Each line contains a timestamp, sender name, and message body. In this step, you will build a parser that turns those raw text lines into structured JavaScript objects, then display the results in the browser.

In this step, get ready to:
  • Parse WhatsApp .txt export files into structured message objects.
  • Build a file upload component using the FileReader API.
  • Render parsed messages as a plain list in the browser.
Create the chat parser module

WhatsApp exports follow different formats depending on the device. iOS wraps timestamps in square brackets, while Android uses a dash separator. Your parser needs to handle both.

  • In VS Code, create a new file called src/parseChat.js inside your whatsapp-chat-viewer project.
  • Add the main parseChat function by pasting the following code:
export function parseChat(text) {
  // Split the file into individual lines
  const lines = text.split('\n');
  const messages = [];
  // Two regex patterns: iOS format (with brackets) and Android format (with dash)
  const patterns = [
    /^\[(\d{1,2}\/\d{1,2}\/\d{2,4},\s\d{1,2}:\d{2}(?::\d{2})?(?:\s[APap][Mm])?)\]\s([^:]+):\s(.*)$/,
    /^(\d{1,2}\/\d{1,2}\/\d{2,4},\s\d{1,2}:\d{2}(?::\d{2})?(?:\s[APap][Mm])?)\s-\s([^:]+):\s(.*)$/,
  ];
  for (const line of lines) {
    // Strip BOM character and whitespace
    const trimmed = line.replace(/^\uFEFF/, '').trim();
    if (!trimmed) continue;
    let matched = false;
    for (const pattern of patterns) {
      const match = trimmed.match(pattern);
      if (match) {
        messages.push({
          timestamp: match[1].trim(),
          sender: match[2].trim(),
          text: match[3].trim(),
        });
        matched = true;
        break;
      }
    }
    // Lines without a timestamp are continuations of the previous message
    if (!matched && messages.length > 0) {
      messages[messages.length - 1].text += '\n' + trimmed;
    }
  }
  return messages;
}

What does this code do?

This function takes the entire file text and processes it line by line.

  • The two patterns match the iOS format ([date] Sender: message) and the Android format (date - Sender: message).
  • Each regex captures three groups: the timestamp, sender name, and message text.
  • Lines that don't match either pattern are treated as continuations of the previous message. This handles multi-line messages correctly.
  • The BOM (byte order mark) strip handles a hidden character that WhatsApp sometimes places at the start of exported files.
  • Below the parseChat function, add these two helper functions that extract date info and calculate chat statistics:
export function getDateFromTimestamp(timestamp) {
  // Extract just the date part (before the comma)
  const parts = timestamp.split(',')[0].trim();
  return parts;
}

export function getChatStats(messages) {
  if (messages.length === 0) return null;

  // Count messages per participant
  const participants = {};
  messages.forEach((msg) => {
    participants[msg.sender] = (participants[msg.sender] || 0) + 1;
  });

  // Get the date range from first and last messages
  const firstDate = getDateFromTimestamp(messages[0].timestamp);
  const lastDate = getDateFromTimestamp(messages[messages.length - 1].timestamp);

  return {
    totalMessages: messages.length,
    participants,
    dateRange: { from: firstDate, to: lastDate },
  };
}

What do these helpers do?

These two utility functions pull useful metadata from the parsed messages.

  • getDateFromTimestamp extracts the date portion (everything before the comma) from a timestamp string like 15/03/2024, 10:30.
  • getChatStats returns an object with the total message count, a breakdown of messages per participant, and the date range of the chat.
  • Save src/parseChat.js.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file, then move on to the next substep.

ⓧ I'd like to double check the full code

Your src/parseChat.js should contain three exported functions in this order:

  • parseChat(text) at the top.
  • getDateFromTimestamp(timestamp) in the middle.
  • getChatStats(messages) at the bottom.

Scroll up and confirm each chunk is present in order. The file should be around 46 lines total.

Replace App.jsx with the file upload component

Now you need a way for users to select a file from their computer. The FileReader API is a built-in browser feature that reads files the user picks through an input element.

  • Open src/App.jsx in VS Code.
  • Select all the existing code and delete it.
  • Paste the following code, which sets up the imports, state, and file upload handler:
import { useState } from 'react';
import { parseChat, getDateFromTimestamp, getChatStats } from './parseChat.js';
import './App.css';

function App() {
  // State for parsed messages, selected user, and chat statistics
  const [messages, setMessages] = useState([]);
  const [currentUser, setCurrentUser] = useState('');
  const [stats, setStats] = useState(null);

  // Read the uploaded file and parse it into message objects
  function handleFileUpload(e) {
    const file = e.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const text = event.target.result;
      const parsed = parseChat(text);
      setMessages(parsed);
      setStats(getChatStats(parsed));
      setCurrentUser('');
    };
    reader.readAsText(file, 'UTF-8');
  }

What does this code do?

  • useState creates three pieces of React state: the array of parsed messages, the currently selected user (for later), and the computed statistics.
  • handleFileUpload fires when the user picks a file. It creates a FileReader, reads the file as UTF-8 text, then passes the text to your parseChat function.
  • The parsed result is stored in state, which causes React to re-render and display the messages.
  • Directly below the handleFileUpload function (still inside the App function), paste the return statement and export:
  return (
    <div>
      <input type="file" accept=".txt" onChange={handleFileUpload} />
      <ul>
        {messages.map((msg, i) => (
          <li key={i}>{msg.sender}: {msg.text}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

What does the JSX render?

  • The <input type="file"> creates a file picker that only accepts .txt files.
  • When the user picks a file, onChange triggers handleFileUpload.
  • The messages.map() renders each parsed message as a simple list item showing the sender and their text.
  • Save src/App.jsx.
  • Switch to your browser at http://localhost:5173. The page should now show a file input button instead of the Vite starter page.

Still seeing the old starter page?

  • Make sure you saved both src/parseChat.js and src/App.jsx. Vite only hot-reloads when files are saved.
  • Check your terminal for red error text. A typo in the regex or a missing closing brace will prevent the page from rendering.
  • Confirm src/parseChat.js is in the src/ folder, not the project root. The import path './parseChat.js' expects it there.

Why isn't my App.jsx rendering after saving?

✔️ Awesome, I've got everything!

Great. Double check you have saved the file, then move on to testing.

ⓧ I'd like to double check the full code

Your src/App.jsx should contain the following sections in order:

  • Three import lines at the top (useState, parseChat functions, App.css).
  • The App function with three useState calls.
  • The handleFileUpload function creating a FileReader.
  • A return block with a file input and a <ul> that maps over messages.
  • export default App at the bottom.

Scroll up and confirm each section is present. The two code blocks you pasted should form one continuous file.

Test the parser with a chat file

Your parser is wired up and the upload component is live. Time to feed it a real file.

Get a test file

If you have a WhatsApp chat export, use that. To export one from your phone, open a chat, tap the menu, then select Export chat and choose Without media.

If you do not have one handy, create a file called test-chat.txt on your Desktop with the sample content below.

  • Create a test file called test-chat.txt on your Desktop with the following content:
[15/03/2024, 10:30:00] Alice: Hey, are you coming to the meeting today?
[15/03/2024, 10:31:00] Bob: Yes! I'll be there in 10 minutes.
[15/03/2024, 10:31:30] Alice: Great, see you soon.
[15/03/2024, 14:05:00] Bob: That meeting was really productive.
[16/03/2024, 09:00:00] Alice: Morning! Did you finish the report?
[16/03/2024, 09:02:00] Bob: Almost done. Just need to add
the final section.
[16/03/2024, 09:03:00] Alice: No rush, take your time.

Why this sample works

This sample uses the iOS format (timestamps in square brackets). It includes messages from two people, spans two days, and has a multi-line message (Bob's "Almost done" reply continues on the next line). This exercises all three parsing features: timestamp extraction, sender detection, and multi-line handling.

Before you test, think about what you expect to see. You have a parser that extracts sender names and message text, and a React component that renders them as list items.

  • In your browser, click the file input button.
  • Select your test-chat.txt file (or a real WhatsApp export).

You should see a plain bulleted list of messages appear below the file input. Each item shows the sender name followed by a colon and their message text. Bob's multi-line message appears as one entry.

No messages appearing after upload?

  • Open your browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). Look for red error messages that point to the problem.
  • If you see an import error mentioning parseChat, confirm the file is named exactly parseChat.js (case-sensitive) and lives inside src/.
  • If using your own WhatsApp export and no messages show, your file may use a locale-specific date format. The parser handles most common formats, but unusual separators may not match.

My file uploads but no messages render on the page. Help me debug.

Your parser works. You can take any WhatsApp chat export and instantly see the messages in your browser. Next up, you will transform this plain list into styled WhatsApp dark-mode message bubbles.

Style Messages as WhatsApp Dark-Mode Bubbles

Your parser works and the messages appear on screen. The problem is they show as a plain unstyled list with no distinction between senders.

In this step, you will turn that flat list into WhatsApp-style message bubbles with a dark-mode color scheme, colored sender names, and timestamps on every message.

In this step, get ready to:
  • Create a ChatMessage component that renders a single styled bubble.
  • Wire the component into your App so it replaces the plain list.
  • Add WhatsApp Web dark-mode CSS for the entire layout.
Create the ChatMessage component

Each message in the chat needs its own visual structure: a sender name, the message body, and a timestamp. A dedicated React component handles this so each bubble is self-contained and reusable.

  • Create a new file called src/ChatMessage.jsx and paste the following component code:
function ChatMessage({ message, isOutgoing }) {
  // Decide bubble style based on whether message is outgoing or incoming
  const bubbleClass = isOutgoing ? 'message-bubble outgoing' : 'message-bubble incoming';
  const time = message.timestamp.split(',')[1]?.trim() || '';

  return (
    <div className={`message-row ${isOutgoing ? 'row-right' : 'row-left'}`}>
      <div className={bubbleClass}>
        {!isOutgoing && <span className="sender-name">{message.sender}</span>}
        <span className="message-text">{message.text}</span>
        <span className="message-time">{time}</span>
      </div>
    </div>
  );
}

export default ChatMessage;

What does this component do?

The component receives a message object (with sender, text, and timestamp fields) and an isOutgoing boolean.

When isOutgoing is true, the bubble aligns right and hides the sender name (you already know who sent it). When false, it aligns left and shows the sender name in color.

  • Save src/ChatMessage.jsx.
Wire ChatMessage into your app

The component exists but your app does not use it yet. You need to import it and replace the plain <ul> list with the new ChatMessage rendering.

  • Open src/App.jsx and find the import section at the top. Add this import on a new line below the existing imports:
import ChatMessage from './ChatMessage.jsx';
  • Now find the entire return block inside the App function. It currently looks like this:
  return (
    <div>
      <input type="file" accept=".txt" onChange={handleFileUpload} />
      <ul>
        {messages.map((msg, i) => (
          <li key={i}>{msg.sender}: {msg.text}</li>
        ))}
      </ul>
    </div>
  );
  • Replace that entire return block with the following, which uses the new ChatMessage component and adds class names for styling:
  return (
    <div className="app-container">
      <div className="chat-area">
        <div className="chat-header">
          <label className="file-upload-label">
            Upload Chat File
            <input type="file" accept=".txt" onChange={handleFileUpload} className="file-input" />
          </label>
        </div>
        <div className="chat-messages">
          {messages.length === 0 ? (
            <div className="empty-state"><p>Upload a WhatsApp chat export file to view the conversation</p></div>
          ) : (
            messages.map((msg, i) => (
              <ChatMessage key={i} message={msg} isOutgoing={msg.sender === currentUser} />
            ))
          )}
        </div>
      </div>
    </div>
  );

What changed here?

  • The file input is now hidden inside a styled <label> that acts as an upload button.
  • When no messages exist, an empty-state placeholder shows.
  • Each message renders through ChatMessage instead of a plain <li>. The isOutgoing prop compares the sender to currentUser (which is still empty, so all messages will be incoming for now).
  • Save src/App.jsx and check your browser.
  • Upload your WhatsApp .txt file again. You should see each message displayed with a sender name and message text, but on a white background with no styling yet.

Not seeing any messages?

  • Make sure the import line says import ChatMessage from './ChatMessage.jsx' with the correct path and file extension.
  • Check that your ChatMessage.jsx file is saved inside the src/ folder, not the project root.
  • Look at the terminal for red error text. A missing closing bracket or mismatched JSX tag will prevent the page from rendering.

Still stuck? Help me debug why my ChatMessage component is not rendering after I upload a file.

Add WhatsApp dark-mode styling

The structure is in place but everything looks like default browser styling. You will now build up src/App.css piece by piece, starting with the color palette and base layout.

  • Create a new file called src/App.css and paste these CSS variables and base styles:
:root {
  --bg-primary: #0b141a;
  --bg-sidebar: #111b21;
  --bg-chat: #0b141a;
  --bg-header: #202c33;
  --bg-incoming: #202c33;
  --bg-outgoing: #005c4b;
  --bg-date-pill: #182229;
  --text-primary: #e9edef;
  --text-secondary: #8696a0;
  --text-timestamp: #8696a0;
  --sender-color: #06cf9c;
  --border-color: #2a3942;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: Segoe UI, Helvetica Neue, Helvetica, Lucida Grande, Arial, Ubuntu, Cantarell, Fira Sans, sans-serif;
  background: var(--bg-primary);
  color: var(--text-primary);
  height: 100vh;
  overflow: hidden;
}

#root { height: 100vh; }

.app-container { display: flex; height: 100vh; }

What do these CSS variables do?

The :root block defines color tokens matching WhatsApp Web's dark theme. Every style below references these variables instead of hardcoded hex codes, making the palette easy to tweak later.

The body gets the dark background (#0b141a) and the Segoe UI font family that WhatsApp Web uses.

  • Save src/App.css and check your browser.
  • The page background should now be a deep dark blue-black, and any text should appear in light gray.
  • Back in src/App.css, add these layout and header styles below the code you just pasted:
.chat-area {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: var(--bg-chat);
  position: relative;
}

.chat-header {
  padding: 12px 20px;
  background: var(--bg-header);
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
}

.file-upload-label {
  padding: 8px 16px;
  background: var(--bg-outgoing);
  color: var(--text-primary);
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  transition: opacity 0.2s;
}

.file-upload-label:hover { opacity: 0.85; }
.file-input { display: none; }

What does this section style?

The .chat-area fills the available width using flexbox. The .chat-header creates a dark toolbar at the top. The upload label is styled as a green button (the same green WhatsApp uses for outgoing messages) and the actual file input is hidden behind it.

  • Save and check the browser. You should see a dark header bar with a green "Upload Chat File" button.
  • Next, add the chat message container and empty-state styles below your existing CSS:
.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px 60px;
}

.empty-state {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: var(--text-secondary);
  font-size: 14px;
}

.message-row { display: flex; margin-bottom: 4px; }
.row-left { justify-content: flex-start; }
.row-right { justify-content: flex-end; }

.message-bubble {
  max-width: 65%;
  padding: 6px 8px 6px 9px;
  border-radius: 8px;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.incoming { background: var(--bg-incoming); border-top-left-radius: 0; }
.outgoing { background: var(--bg-outgoing); border-top-right-radius: 0; }

What do the bubble styles do?

  • The .chat-messages container scrolls independently so long chats stay navigable.
  • Each .message-bubble has a max width of 65% so it never stretches across the full page.
  • Incoming bubbles use dark gray (#202c33) and outgoing bubbles use green (#005c4b). The flat corner on each side mimics WhatsApp's speech-bubble shape.
  • Save and upload a chat file. Messages should now appear as dark gray rounded rectangles on the left side of the screen.
  • Finally, add the text styling for content inside the bubbles:
.sender-name {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--sender-color);
  margin-bottom: 2px;
}

.message-text {
  font-size: 14.2px;
  line-height: 1.4;
  color: var(--text-primary);
  white-space: pre-wrap;
  word-wrap: break-word;
}

.message-time {
  font-size: 11px;
  color: var(--text-timestamp);
  align-self: flex-end;
  margin-top: 2px;
}

What do these text styles do?

  • Sender names appear in teal (#06cf9c) to stand out from the message body.
  • The white-space: pre-wrap on message text preserves line breaks from multi-line messages.
  • Timestamps align to the bottom-right of each bubble in a small muted font.
  • Save src/App.css.

Before you check the browser, what do you expect to see? Think about which side the messages will appear on, given that currentUser is still an empty string.

Upload your chat file. Every message appears as a dark gray bubble on the left side with a teal sender name and a small timestamp in the bottom-right corner. No messages appear on the right because the app does not yet know which participant is "you." That is the gap you will close in the next step.

Bubbles not styled or layout looks broken?

  • Confirm your App.jsx has import './App.css' near the top. Without this import, Vite cannot inject the styles.
  • Check that the class names in your JSX (app-container, chat-area, chat-messages) match the CSS selectors exactly. A single typo will break the connection.
  • If the page is completely white, open DevTools (F12) and check the Console tab for syntax errors in your CSS file.

Need more help? My WhatsApp chat viewer bubbles are not styled correctly even though I added App.css.

✔️ Awesome, I've got everything!

Great. Double check that you have saved all three files: src/ChatMessage.jsx, src/App.css, and src/App.jsx.

ⓧ I'd like to double check the full code

Here is the complete src/ChatMessage.jsx:

function ChatMessage({ message, isOutgoing }) {
  const bubbleClass = isOutgoing ? 'message-bubble outgoing' : 'message-bubble incoming';
  const time = message.timestamp.split(',')[1]?.trim() || '';

  return (
    <div className={`message-row ${isOutgoing ? 'row-right' : 'row-left'}`}>
      <div className={bubbleClass}>
        {!isOutgoing && <span className="sender-name">{message.sender}</span>}
        <span className="message-text">{message.text}</span>
        <span className="message-time">{time}</span>
      </div>
    </div>
  );
}

export default ChatMessage;

For src/App.css, scroll up and confirm you have all four sections in order: CSS variables and base styles, layout and header styles, message container and bubble styles, and text styles for sender/message/time.

For src/App.jsx, confirm you have the ChatMessage import at the top, and the return block uses app-container > chat-area > chat-header + chat-messages structure with the conditional rendering.

Your messages now look like real WhatsApp bubbles on a dark background. The one thing missing is obvious: every message sits on the left. Next up, you will build a sidebar that lets you pick which participant is "you" so their messages shift to the right in green.

Build the Sidebar with Stats and Bubble-Side Selector

Right now, every message in your chat viewer appears on the left side. The app has no way to tell which participant is "you" versus everyone else.

In this step, you will build a sidebar panel that displays chat statistics and provides a dropdown to select your name. Once a name is selected, that person's messages shift to the right with green outgoing bubbles, making the conversation instantly readable.

In this step, get ready to:
  • Create a Sidebar component that displays message counts, participants, and date range.
  • Add sidebar styles to your CSS for the two-panel layout.
  • Wire the Sidebar into App so the dropdown controls which messages appear on the right.
Create the Sidebar component

The Sidebar needs to handle two states. Before a file is uploaded, it shows a simple hint message. After a file is uploaded, it displays chat statistics, a list of participants, and a dropdown to pick who appears on the right side.

  • Create a new file called src/Sidebar.jsx and add this code to start the component with its empty-state branch and the beginning of the stats display:
function Sidebar({ stats, currentUser, onUserChange }) {
  // Show placeholder when no file is uploaded
  if (!stats) {
    return (
      <div className="sidebar">
        <div className="sidebar-header">
          <h2>WhatsApp Chat Viewer</h2>
        </div>
        <div className="sidebar-content">
          <p className="upload-hint">Upload a WhatsApp exported .txt file to get started</p>
        </div>
      </div>
    );
  }

  // Extract participant names from the stats object
  const participantNames = Object.keys(stats.participants);

  return (
    <div className="sidebar">
      <div className="sidebar-header">
        <h2>WhatsApp Chat Viewer</h2>
      </div>
      <div className="sidebar-content">

What does this code do?

The component accepts three props: stats (the chat statistics object), currentUser (the selected participant name), and onUserChange (a callback to update the selection).

The if (!stats) check handles conditional rendering. Before a file is uploaded, stats is null, so the component renders a placeholder hint instead of crashing on missing data.

  • Below the opening <div className="sidebar-content"> line, add the stats section that displays the chat metadata:
        <div className="stats-section">
          <h3>Chat Info</h3>
          <div className="stat-item">
            <span className="stat-label">Total Messages</span>
            <span className="stat-value">{stats.totalMessages}</span>
          </div>
          <div className="stat-item">
            <span className="stat-label">Participants</span>
            <span className="stat-value">{participantNames.length}</span>
          </div>
          <div className="stat-item">
            <span className="stat-label">From</span>
            <span className="stat-value">{stats.dateRange.from}</span>
          </div>
          <div className="stat-item">
            <span className="stat-label">To</span>
            <span className="stat-value">{stats.dateRange.to}</span>
          </div>
        </div>

What does the stats section show?

Each stat-item div creates a row with a label on the left and a value on the right. The values come directly from the getChatStats() function you built in parseChat.js earlier.

  • Below the stats section, add the participants list and the dropdown selector to complete the component:
        {/* List each participant with their message count */}
        <div className="participants-section">
          <h3>Participants</h3>
          {participantNames.map((name) => (
            <div key={name} className="participant-item">
              <span className="participant-name">{name}</span>
              <span className="participant-count">{stats.participants[name]}</span>
            </div>
          ))}
        </div>

        {/* Dropdown to pick who appears on the right side */}
        <div className="selector-section">
          <h3>Show on Right Side</h3>
          <select
            value={currentUser}
            onChange={(e) => onUserChange(e.target.value)}
            className="user-select"
          >
            <option value="">-- Select your name --</option>
            {participantNames.map((name) => (
              <option key={name} value={name}>
                {name}
              </option>
            ))}
          </select>
        </div>
      </div>
    </div>
  );
}

export default Sidebar;

How does the dropdown work?

The <select> element is a controlled component. Its value is bound to currentUser, and its onChange calls onUserChange with the newly selected name. This sends the chosen name back up to App, which then re-renders the chat with updated alignment.

  • Save src/Sidebar.jsx.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

Your src/Sidebar.jsx should contain all three sections in this order:

  • The if (!stats) early return that renders the upload hint.
  • The stats section with four stat-items (Total Messages, Participants, From, To).
  • The participants section mapping over names, followed by the selector section with the <select> dropdown and the export default Sidebar at the bottom.
Connect the Sidebar to your app

The Sidebar component exists, but App does not know about it yet. You need to import it and render it inside the layout.

  • In src/App.jsx, find this import line:
import ChatMessage from './ChatMessage.jsx';
  • Add this new import directly below it:
import Sidebar from './Sidebar.jsx';
  • Next, find this line inside the return statement:
    <div className="app-container">
  • Directly below it (before the <div className="chat-area"> line), add the Sidebar component:
      <Sidebar stats={stats} currentUser={currentUser} onUserChange={setCurrentUser} />

How do the props connect?

You pass stats so the Sidebar can display the numbers. currentUser keeps the dropdown in sync with App's state. onUserChange={setCurrentUser} means that when the dropdown fires, it calls setCurrentUser directly, updating which messages appear on the right.

  • Save src/App.jsx and check your browser.
  • You should see the sidebar text appearing on the left side of the page. It will be unstyled (just plain text on the dark background) because we have not added the sidebar CSS yet.

Don't see any sidebar text?

Make sure you saved both src/Sidebar.jsx and src/App.jsx. Check your terminal for errors. A common cause is a missing closing brace or parenthesis in Sidebar.jsx.

help me fix my Sidebar component import

Style the sidebar panel

The sidebar content is rendering, but it needs proper layout and styling to match the WhatsApp Web dark theme. You will add CSS that gives the sidebar a fixed width and styles each section.

  • In src/App.css, find the .app-container rule. Directly below it, add these sidebar layout styles:
.sidebar {
  width: 30%;
  min-width: 280px;
  max-width: 400px;
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

.sidebar-header {
  padding: 16px 20px;
  background: var(--bg-header);
  border-bottom: 1px solid var(--border-color);
}

.sidebar-header h2 { font-size: 16px; font-weight: 500; color: var(--text-primary); }

.sidebar-content {
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.upload-hint { color: var(--text-secondary); font-size: 14px; text-align: center; padding: 40px 0; }

.stats-section h3, .participants-section h3, .selector-section h3 {
  font-size: 13px;
  text-transform: uppercase;
  color: var(--sender-color);
  margin-bottom: 10px;
  letter-spacing: 0.5px;
}

What do these styles achieve?

The .sidebar class sets the panel to 30% of the viewport width, creating the two-column layout. The overflow-y: auto ensures the sidebar scrolls independently if the content is taller than the screen.

Section headings (h3 elements) use the teal green --sender-color with uppercase text to create clear visual separators between stats, participants, and the selector.

  • Below the styles you just added, add the stat items and participant card styles:
.stat-item { display: flex; justify-content: space-between; padding: 6px 0; font-size: 14px; }
.stat-label { color: var(--text-secondary); }
.stat-value { color: var(--text-primary); font-weight: 500; }

.participant-item {
  display: flex;
  justify-content: space-between;
  padding: 8px 12px;
  background: var(--bg-header);
  border-radius: 8px;
  margin-bottom: 6px;
  font-size: 14px;
}

.participant-name { color: var(--text-primary); }
.participant-count { color: var(--text-secondary); font-size: 12px; }

.user-select {
  width: 100%;
  padding: 10px 12px;
  background: var(--bg-header);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  font-size: 14px;
  cursor: pointer;
}

.user-select:focus { outline: none; border-color: var(--sender-color); }

What does the user-select rule do?

The .user-select styles the dropdown to blend into the dark theme. On focus, the border changes to the teal accent color so the learner knows which control is active.

  • Save src/App.css.

Before you test, think about what you expect to happen. If you upload a chat file now and select a name from the dropdown, which side should that person's messages appear on?

  • Upload your WhatsApp .txt file using the upload button.
  • Look at the sidebar. You should see the total message count, the number of participants, the date range (From/To), and each participant listed with their message count.
  • Select a participant name from the Show on Right Side dropdown.

That person's messages now appear on the right side with green outgoing bubbles. Everyone else stays on the left with dark gray incoming bubbles. Change the dropdown selection and the entire chat re-aligns instantly.

Messages not shifting to the right?

Make sure the <Sidebar> component in App.jsx passes onUserChange={setCurrentUser} (not onUserChange={currentUser}). The prop must be the setter function, not the current value.

Also confirm that in ChatMessage.jsx, the isOutgoing prop drives the row-right and outgoing classes. If you see all messages on the left even after selecting a name, check the browser console for errors.

help me debug my bubble-side selector

Your sidebar is working and the conversation is finally readable with proper left/right alignment. Next up, you will add date separator pills between messages from different days to give the chat a clear timeline.

Add Date Separators and Final Polish

In the previous step, you built the sidebar with stats and a bubble-side selector. Messages now align left or right based on who you pick from the dropdown.

Long chats spanning multiple days are hard to follow without visual date breaks. Scrolling through 200 messages with no sense of when each one was sent turns the conversation into a wall of text.

In this step, you will add date separator pills between messages from different days and apply the finishing touches that make your viewer look like the real WhatsApp Web.

In this step, get ready to:
  • Create a DateSeparator component that renders centered date pills.
  • Update the message rendering logic to insert date breaks between days.
  • Add the WhatsApp wallpaper pattern and style the date pills.
Create the DateSeparator component

WhatsApp shows a small translucent pill with the date whenever the conversation crosses into a new day. You need a dedicated component for this so you can insert it between message groups.

  • Create a new file called src/DateSeparator.jsx and add the following code:
function DateSeparator({ date }) {
  return (
    <div className="date-separator">
      <span className="date-pill">{date}</span>
    </div>
  );
}

export default DateSeparator;

What does this component do?

DateSeparator takes a single date prop (a string like "15/03/2024") and renders it inside a centered pill shape.

The CSS classes will handle the visual styling. For now the component just provides the DOM structure.

  • Save src/DateSeparator.jsx.
Wire date separators into the message rendering

Currently, App.jsx renders messages with a simple .map() call. You need a smarter rendering function that checks each message's date and inserts a separator whenever the day changes.

  • In src/App.jsx, find this import line:
import ChatMessage from './ChatMessage.jsx';
  • Add this new import directly below it:
import DateSeparator from './DateSeparator.jsx';
  • Now add the renderMessages function inside the App component, directly after the handleFileUpload function. Copy the following code:
  function renderMessages() {
    if (messages.length === 0) {
      return (
        <div className="empty-state">
          <p>Upload a WhatsApp chat export file to view the conversation</p>
        </div>
      );
    }

    const elements = [];
    let lastDate = '';

    messages.forEach((msg, index) => {
      const msgDate = getDateFromTimestamp(msg.timestamp);

      if (msgDate !== lastDate) {
        elements.push(<DateSeparator key={`date-${index}`} date={msgDate} />);
        lastDate = msgDate;
      }

      elements.push(
        <ChatMessage
          key={index}
          message={msg}
          isOutgoing={msg.sender === currentUser}
        />
      );
    });

    return elements;
  }

What does this function do?

  • It loops through all parsed messages and tracks the last date it saw with lastDate.
  • Whenever the date changes (a new day starts), it pushes a DateSeparator element into the array before pushing the message itself.
  • getDateFromTimestamp extracts just the date portion (e.g. "15/03/2024") from the full timestamp string.

Now you need to replace the inline rendering logic in the JSX with a call to this new function.

  • Scroll down to the return statement in the App component. Find this block inside <div className="chat-messages">:
        <div className="chat-messages">
          {messages.length === 0 ? (
            <div className="empty-state"><p>Upload a WhatsApp chat export file to view the conversation</p></div>
          ) : (
            messages.map((msg, i) => (
              <ChatMessage key={i} message={msg} isOutgoing={msg.sender === currentUser} />
            ))
          )}
        </div>
  • Replace that entire <div className="chat-messages"> block with this simpler version:
        <div className="chat-messages">
          {renderMessages()}
        </div>
  • Save src/App.jsx and refresh your browser.
  • Upload your WhatsApp .txt file. You should see date text (like "15/03/2024") appearing between groups of messages from different days. The dates are unstyled plain text for now.

Don't see any date text between messages?

  • Make sure you imported DateSeparator at the top of App.jsx. A missing import causes a runtime error.
  • Check your browser console for errors. If you see "DateSeparator is not defined", the import path might have a typo.
  • If your chat file only spans one day, you will only see one date above all messages. Try a file with messages from multiple days.

Why aren't my date separators showing between messages?

Add the WhatsApp wallpaper and date pill styles

The date separators work logically but look like plain text. You also need the subtle dot-pattern wallpaper that WhatsApp Web shows behind messages. Both are CSS additions.

  • In src/App.css, find the .chat-messages rule. It currently looks like this:
.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px 60px;
}
  • Add the background-image property so the rule looks like this:
.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px 60px;
  background-image: url("data:image/svg+xml,%3Csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='p' width='40' height='40' patternUnits='userSpaceOnUse'%3E%3Cpath d='M20 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23ffffff' fill-opacity='0.03'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='200' height='200' fill='%23080f14'/%3E%3Crect width='200' height='200' fill='url(%23p)'/%3E%3C/svg%3E");
}

What is this background-image?

This is an inline SVG encoded as a data URL. It creates a subtle repeating dot pattern on a very dark background, mimicking the WhatsApp Web chat wallpaper. Using an inline SVG avoids needing an external image file.

  • Now scroll to the very bottom of src/App.css (after the .message-time rule) and add the date separator styles:
.date-separator {
  display: flex;
  justify-content: center;
  margin: 16px 0;
}

.date-pill {
  background: var(--bg-date-pill);
  color: var(--text-secondary);
  padding: 6px 14px;
  border-radius: 8px;
  font-size: 12px;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}

What do these styles do?

  • .date-separator centers the pill horizontally and adds vertical spacing between message groups.
  • .date-pill uses the dark translucent background defined in your CSS variables. The small border-radius and subtle box-shadow match WhatsApp Web's badge styling.
  • Save src/App.css.

Before you check the browser, what do you expect to see differently in the chat area now?

  • Refresh your browser and upload your .txt file again.

You should see dark rounded pills with dates (e.g. "15/03/2024") appearing between messages from different days. The chat background now has a subtle dot pattern. The overall layout shows the sidebar on the left (30% width) and the chat area on the right (70%), with the chat scrolling independently.

Date pills not styled or wallpaper missing?

  • If the date text is still unstyled, confirm you added both .date-separator and .date-pill classes at the bottom of App.css. Class names are case-sensitive.
  • If the wallpaper does not appear, check that the background-image property is inside the .chat-messages rule's curly braces, not outside them.
  • The SVG data URL is long. If you see a CSS parse error in DevTools, make sure you copied the entire string including the closing parenthesis and semicolon.

Why isn't my WhatsApp wallpaper or date pill styling showing up?

✔️ Awesome, I've got everything!

Great. Double check you have saved both src/App.jsx, src/App.css, and src/DateSeparator.jsx.

ⓧ I'd like to double check the full code

Here is the full src/DateSeparator.jsx:

function DateSeparator({ date }) {
  return (
    <div className="date-separator">
      <span className="date-pill">{date}</span>
    </div>
  );
}

export default DateSeparator;

Here is the full src/App.jsx. Confirm your imports, the renderMessages function, and the JSX all match:

import { useState } from 'react';
import { parseChat, getDateFromTimestamp, getChatStats } from './parseChat.js';
import ChatMessage from './ChatMessage.jsx';
import DateSeparator from './DateSeparator.jsx';
import Sidebar from './Sidebar.jsx';
import './App.css';
function App() {
  const [messages, setMessages] = useState([]);
  const [currentUser, setCurrentUser] = useState('');
  const [stats, setStats] = useState(null);

  function handleFileUpload(e) {
    const file = e.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const text = event.target.result;
      const parsed = parseChat(text);
      setMessages(parsed);
      setStats(getChatStats(parsed));
      setCurrentUser('');
    };
    reader.readAsText(file, 'UTF-8');
  }
  function renderMessages() {
    if (messages.length === 0) {
      return (
        <div className="empty-state">
          <p>Upload a WhatsApp chat export file to view the conversation</p>
        </div>
      );
    }

    const elements = [];
    let lastDate = '';

    messages.forEach((msg, index) => {
      const msgDate = getDateFromTimestamp(msg.timestamp);

      if (msgDate !== lastDate) {
        elements.push(<DateSeparator key={`date-${index}`} date={msgDate} />);
        lastDate = msgDate;
      }

      elements.push(
        <ChatMessage
          key={index}
          message={msg}
          isOutgoing={msg.sender === currentUser}
        />
      );
    });

    return elements;
  }
  return (
    <div className="app-container">
      <Sidebar
        stats={stats}
        currentUser={currentUser}
        onUserChange={setCurrentUser}
      />
      <div className="chat-area">
        <div className="chat-header">
          <label className="file-upload-label">
            Upload Chat File
            <input
              type="file"
              accept=".txt"
              onChange={handleFileUpload}
              className="file-input"
            />
          </label>
        </div>
        <div className="chat-messages">
          {renderMessages()}
        </div>
      </div>
    </div>
  );
}

export default App;

For src/App.css, confirm the .chat-messages rule has the background-image property, and the .date-separator and .date-pill rules are at the bottom of the file:

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px 60px;
  background-image: url("data:image/svg+xml,%3Csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='p' width='40' height='40' patternUnits='userSpaceOnUse'%3E%3Cpath d='M20 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23ffffff' fill-opacity='0.03'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='200' height='200' fill='%23080f14'/%3E%3Crect width='200' height='200' fill='url(%23p)'/%3E%3C/svg%3E");
}
.date-separator {
  display: flex;
  justify-content: center;
  margin: 16px 0;
}

.date-pill {
  background: var(--bg-date-pill);
  color: var(--text-secondary);
  padding: 6px 14px;
  border-radius: 8px;
  font-size: 12px;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}

Your WhatsApp Chat Viewer is complete. It reads real exported chat files, displays messages with proper sender colors and timestamps, aligns bubbles based on who you select, and divides the conversation by day with date pills. The whole thing runs locally in your browser with zero backend. Next up, the Secret Mission challenges you to add search and filtering on top of what you have built.

Secret mission

Add Search and Participant Filter

Your chat viewer works, but scrolling through hundreds of messages to find a specific conversation is tedious. In this mission, add a search bar and participant filter to the sidebar so you can instantly find any message in the chat. You will also highlight matching text directly inside the message bubbles.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely on your local machine, so there are no ongoing costs.

Resources you used:

  • The whatsapp-chat-viewer project folder on your Desktop (includes all source files and node_modules).
  • The Vite dev server running in your terminal.

Keep everything running

No action needed. Choose this if you are still actively building or want to use the chat viewer.

  • Your dev server stays running at http://localhost:5173.
  • To restart the viewer later, open a terminal in the whatsapp-chat-viewer folder and run npm run dev.

Pause - I'll come back to this later

Shut down the running dev server to free up your terminal. Your project files stay on disk so you can pick up where you left off.

  • Switch to the terminal where the dev server is running.
  • Press Ctrl+C to stop the server.
  • To resume later, open a terminal in ~/Desktop/whatsapp-chat-viewer and run npm run dev.

Delete - I don't want to use this again

Remove all project files and start fresh. This is permanent.

  • Stop the dev server by pressing Ctrl+C in the terminal where it is running.
  • Delete the entire project folder by running the command for your operating system below.

macOS

rm -rf ~/Desktop/whatsapp-chat-viewer

Windows

rmdir /s /q %USERPROFILE%\Desktop\whatsapp-chat-viewer
  • Confirm the folder is gone by running ls ~/Desktop (macOS) or dir %USERPROFILE%\Desktop (Windows). You should no longer see whatsapp-chat-viewer in the output.

Nice Work!

Nice Work!

Nice work! You just built a fully functional WhatsApp Chat Viewer from scratch using React and Vite. Upload any exported chat file and instantly relive the conversation in a pixel-accurate dark-mode interface.

You learned how to:

  • Parsed real WhatsApp exported .txt files using regex pattern matching and the FileReader API, handling multiple export formats, multi-line messages, and BOM characters.
  • Created a WhatsApp Web dark-mode UI with styled message bubbles, colored sender names, timestamps, and date separator pills that make long conversations easy to follow.
  • Built an interactive sidebar with chat statistics and a bubble-side selector that dynamically re-aligns all messages in real time based on your selection.
  • Secret Mission: Implemented search and participant filtering with real-time message highlighting, turning the viewer into a genuinely useful tool for finding specific conversations.

Ready to quiz yourself?