# How to format raw Telegram chat data?

When building CRM or support systems, you often need to import and display chat conversations from external sources like Telegram. Raw JSON chat data is difficult to read, but with proper formatting, you can create a beautiful, threaded conversation view.

This guide shows you how to parse and display Telegram chat JSON data in a readable, styled format with message threading, timestamps, and text formatting.

## Overview[​](#overview "Direct link to Overview")

The process involves:

1. storing the raw JSON chat data in a text field
2. creating a custom layout with CSS styling for the chat interface
3. parsing the JSON data in view logic
4. formatting message text entities (bold, italic, links, etc.)
5. handling message replies and threading
6. rendering the formatted HTML

## Sample chat data structure[​](#sample-chat-data-structure "Direct link to Sample chat data structure")

Telegram chat exports use a JSON structure like this:

```
{
  "name": "MobileCarrier Support",
  "type": "personal_chat",
  "id": 987654321,
  "messages": [
    {
      "id": 1,
      "type": "message",
      "date": "2024-10-08T14:23:15",
      "from": "John Miller",
      "from_id": "user445566",
      "text": "Hi, I just received my bill...",
      "text_entities": [
        {
          "type": "plain",
          "text": "Hi, I just received my bill..."
        }
      ]
    },
    {
      "id": 2,
      "type": "message",
      "date": "2024-10-08T14:24:32",
      "from": "Sarah - Support Agent",
      "from_id": "user778899",
      "reply_to_message_id": 1,
      "text": [
        "Thank you for that information. These were ",
        {
          "type": "bold",
          "text": "incoming calls"
        },
        " that you answered."
      ]
    }
  ]
}
```

Key elements:

* **messages array**: contains all conversation messages
* **text**: can be a simple string or an array with formatting entities
* **text\_entities**: defines formatting like bold, italic, links
* **reply\_to\_message\_id**: references parent message for threading

## Implementation[​](#implementation "Direct link to Implementation")

### Step 1: Define the field for raw JSON[​](#step-1-define-the-field-for-raw-json "Direct link to Step 1: Define the field for raw JSON")

In your `fields/index.ts`, add a field to store the raw chat data:

```
{
  caption: 'Chat JSON Data',
  name: 'c_json_data',
  type: 'text',
}
```

### Step 2: Create the default layout[​](#step-2-create-the-default-layout "Direct link to Step 2: Create the default layout")

In `views/layouts/default.tsx`, organize your layout with tabs:

```
/** @jsx entity */
import { entity } from "#typings";

export default (
  <layout>
    <section>
      <column>
        <field name="c_contact" />
      </column>
      <column>
        <field name="creation_date" />
      </column>
    </section>
    <section tab="Conversation" captionAboveField="true" visibility="true">
      <column>
        <custom-layout name="custom_layout_thread" widthFactor="3" />
      </column>
    </section>
    <section tab="Technical" captionAboveField="true">
      <column>
        <field name="c_json_data" widthFactor="3" />
      </column>
    </section>
  </layout>
);
```

This creates:

* a "Conversation" tab with the formatted chat view
* a "Technical" tab showing the raw JSON data

### Step 3: Create the custom layout with styling[​](#step-3-create-the-custom-layout-with-styling "Direct link to Step 3: Create the custom layout with styling")

Create `views/layouts/custom_layout_thread.tsx` with styled chat interface:

```
/** @jsx entity */
import { entity } from "#typings";

export default (
  <layout>
    <style jsx>
      {`
        .thread-frame {
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
            Helvetica, Arial, sans-serif;
          background-color: #ffffff;
          color: #333333;
          margin: 0;
          padding: 20px;
          font-size: 14px;
        }
        .thread-frame .chat-container {
          max-width: 800px;
          margin: 0 auto;
          background-color: #f8f9fa;
          border-radius: 12px;
          overflow: hidden;
          box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }
        .thread-frame .chat-header {
          background-color: #e3f2fd;
          padding: 20px;
          border-bottom: 1px solid #e1e8ed;
        }
        .thread-frame .chat-title {
          font-size: 20px;
          font-weight: 600;
          margin: 0;
        }
        .thread-frame .chat-type {
          font-size: 16px;
          color: #657786;
          margin-top: 4px;
        }
        .thread-frame .messages {
          padding: 20px;
        }
        .thread-frame .message {
          margin-bottom: 20px;
          display: flex;
          flex-direction: column;
        }
        .thread-frame .message-header {
          display: flex;
          align-items: baseline;
          margin-bottom: 6px;
        }
        .thread-frame .sender {
          font-weight: 600;
          color: #1976d2;
          margin-right: 10px;
        }
        .thread-frame .timestamp {
          font-size: 14px;
          color: #8899a6;
        }
        .thread-frame .message-content {
          background-color: #f1f3f4;
          padding: 10px 14px;
          border-radius: 8px;
          line-height: 1.5;
          max-width: 85%;
        }
        .thread-frame .reply-to {
          background-color: #f8f9fa;
          border-left: 3px solid #1976d2;
          padding: 6px 10px;
          margin-bottom: 8px;
          font-size: 15px;
          color: #5f6368;
          border-radius: 4px;
        }
        .thread-frame .mention {
          color: #1976d2;
        }
        .thread-frame .hashtag {
          color: #1976d2;
        }
        .thread-frame code {
          background-color: #f1f3f4;
          padding: 2px 6px;
          border-radius: 4px;
          font-family: "Courier New", monospace;
        }
        .thread-frame pre {
          background-color: #f1f3f4;
          padding: 10px;
          border-radius: 4px;
          overflow-x: auto;
          font-family: "Courier New", monospace;
        }
        .thread-frame a {
          color: #1976d2;
          text-decoration: none;
        }
        .thread-frame a:hover {
          text-decoration: underline;
        }
      `}
    </style>

    <div class="thread-frame">
      <div ngBindHtml="ui.getChatHtml() | safeHtml"></div>
    </div>
  </layout>
);
```

Key styling features:

* **Modern design**: clean, card-based interface with shadows and rounded corners
* **Message bubbles**: distinct background for each message with padding
* **Color coding**: blue for senders and links, gray for timestamps
* **Reply indicators**: bordered section showing quoted messages
* **Code formatting**: special styling for code blocks
* **Responsive**: max-width container centered on screen

### Step 4: Implement the parsing logic[​](#step-4-implement-the-parsing-logic "Direct link to Step 4: Implement the parsing logic")

Create `views/logic/index.ts` to process the JSON data:

```
import { entity } from "#typings";
import type { ViewLogic } from "@comind/api";
import moment from "moment";

export default {
  onReady,
} satisfies ViewLogic<typeof entity>;

function onReady() {
  ComindView.ui.getChatHtml = getChatHtml;
}

function getChatHtml() {
  const jsonData = JSON.parse(entity.c_json_data || "{}");
  let html = "";

  for (const message of jsonData.messages) {
    let replyContent = "";
    if (message.reply_to_message_id) {
      const replyMsg = jsonData.messages.find(
        (m: any) => m.id === message.reply_to_message_id
      );
      if (replyMsg) {
        const replyText =
          typeof replyMsg.text === "string"
            ? replyMsg.text
            : processTextEntities(replyMsg.text);
        replyContent = `<div class="reply-to">↩️ Reply to: ${replyText.slice(
          0,
          80
        )}${replyText.length > 80 ? "..." : ""}</div>`;
      }
    }

    const messageText = processTextEntities(message.text);
    html += `
      <div class="message">
        <div class="message-header">
          <span class="sender">${message.from}</span>
          <span class="timestamp">${moment(message.date).format(
            "MMMM Do YYYY, h:mm a"
          )}</span>
        </div>
        ${replyContent}
        <div class="message-content">
          ${messageText}
        </div>
      </div>
    `;
  }
  return html;
}

function processTextEntities(text: any) {
  if (typeof text === "string") return text;
  if (!Array.isArray(text)) return "";

  return text
    .map((item) => {
      if (typeof item === "string") return item;
      switch (item.type) {
        case "bold":
          return `<strong>${item.text}</strong>`;
        case "italic":
          return `<em>${item.text}</em>`;
        case "underline":
          return `<u>${item.text}</u>`;
        case "strikethrough":
          return `<s>${item.text}</s>`;
        case "code":
          return `<code>${item.text}</code>`;
        case "pre":
          return `<pre>${item.text}</pre>`;
        case "link":
          return `<a href="${item.text}" target="_blank">${item.text}</a>`;
        case "mention":
          return `<span class="mention">${item.text}</span>`;
        case "hashtag":
          return `<span class="hashtag">${item.text}</span>`;
        default:
          return item.text;
      }
    })
    .join("");
}
```

## How it works[​](#how-it-works "Direct link to How it works")

### 1. JSON parsing[​](#1-json-parsing "Direct link to 1. JSON parsing")

The `getChatHtml()` function:

* parses the raw JSON from `entity.c_json_data`
* iterates through all messages
* builds HTML for each message
* returns the complete formatted HTML string

### 2. Reply threading[​](#2-reply-threading "Direct link to 2. Reply threading")

For messages with `reply_to_message_id`:

* finds the referenced message by ID
* extracts the first 80 characters as a preview
* displays it in a styled reply indicator above the message

### 3. Text entity processing[​](#3-text-entity-processing "Direct link to 3. Text entity processing")

The `processTextEntities()` function handles two formats:

* **Simple string**: returns as-is
* **Array format**: processes each element and applies formatting

Supported entity types:

* `bold` → `<strong>` tags
* `italic` → `<em>` tags
* `underline` → `<u>` tags
* `strikethrough` → `<s>` tags
* `code` → inline `<code>` tags
* `pre` → code block `<pre>` tags
* `link` → clickable `<a>` tags
* `mention` → styled spans for @mentions
* `hashtag` → styled spans for #hashtags

### 4. Timestamp formatting[​](#4-timestamp-formatting "Direct link to 4. Timestamp formatting")

Uses the `moment` library to format ISO dates into human-readable format:

```
moment(message.date).format("MMMM Do YYYY, h:mm a");
// Output: "October 8th 2024, 2:23 pm"
```

### 5. HTML rendering[​](#5-html-rendering "Direct link to 5. HTML rendering")

The custom layout uses Angular's `ngBindHtml` directive with `safeHtml` filter:

```
<div ngBindHtml="ui.getChatHtml() | safeHtml"></div>
```

This safely renders the generated HTML with proper sanitization.

## Customization options[​](#customization-options "Direct link to Customization options")

### Changing timestamp format[​](#changing-timestamp-format "Direct link to Changing timestamp format")

Modify the moment format string:

```
// 24-hour format
moment(message.date).format("DD/MM/YYYY HH:mm");
// Output: "08/10/2024 14:23"

// Relative time
moment(message.date).fromNow();
// Output: "2 hours ago"

// Short format
moment(message.date).format("MMM D, h:mm A");
// Output: "Oct 8, 2:23 PM"
```

### Supporting different sender styles[​](#supporting-different-sender-styles "Direct link to Supporting different sender styles")

Style messages differently based on sender:

```
function getChatHtml() {
  const jsonData = JSON.parse(entity.c_json_data || "{}");
  let html = "";

  for (const message of jsonData.messages) {
    const isAgent = message.from.includes("Support Agent");
    const messageClass = isAgent ? "message-agent" : "message-customer";

    html += `
      <div class="message ${messageClass}">
        <!-- message content -->
      </div>
    `;
  }

  return html;
}
```

Then add corresponding CSS:

```
.thread-frame .message-agent .message-content {
  background-color: #e3f2fd;
  margin-left: auto;
}

.thread-frame .message-customer .message-content {
  background-color: #f1f3f4;
  margin-right: auto;
}
```

### Adding chat header with metadata[​](#adding-chat-header-with-metadata "Direct link to Adding chat header with metadata")

Display chat information at the top:

```
function getChatHtml() {
  const jsonData = JSON.parse(entity.c_json_data || "{}");

  let html = `
    <div class="chat-container">
      <div class="chat-header">
        <div class="chat-title">${jsonData.name || "Conversation"}</div>
        <div class="chat-type">${jsonData.type || "Chat"} • ${
    jsonData.messages?.length || 0
  } messages</div>
      </div>
      <div class="messages">
  `;

  for (const message of jsonData.messages) {
    // ... message rendering
  }

  html += `
      </div>
    </div>
  `;

  return html;
}
```

## Error handling[​](#error-handling "Direct link to Error handling")

Add error handling for malformed JSON:

```
function getChatHtml() {
  try {
    const jsonData = JSON.parse(entity.c_json_data || "{}");

    if (!jsonData.messages || !Array.isArray(jsonData.messages)) {
      return '<div style="color: #e53935;">Invalid chat data: messages array not found</div>';
    }

    // ... rest of processing
  } catch (error) {
    return `<div style="color: #e53935;">Error parsing chat data: ${error.message}</div>`;
  }
}
```

## Use cases[​](#use-cases "Direct link to Use cases")

This approach is particularly useful for:

* **Customer support**: displaying imported support conversations in your CRM
* **Chat archives**: creating searchable archives of Telegram conversations
* **Compliance**: maintaining formatted records of business communications
* **Data migration**: transferring chat history from Telegram to your system
* **Analysis**: reviewing conversation flows and support quality
* **Documentation**: preserving important discussions with proper formatting

## Related[​](#related "Direct link to Related")

* [View logic](/developer-guide/building-blocks/view-logic.md) - `onReady` and other lifecycle hooks used to set `ComindView.ui` properties
* [Form layouts](/developer-guide/building-blocks/form-layouts.md) - custom layouts, `<style jsx>`, and `ngBindHtml` for rendering dynamic HTML

## Advanced scenarios[​](#advanced-scenarios "Direct link to Advanced scenarios")

### Exporting to other formats[​](#exporting-to-other-formats "Direct link to Exporting to other formats")

Convert chat data to different formats:

```
function exportToCsv() {
  const jsonData = JSON.parse(entity.c_json_data || "{}");
  const rows = [["Date", "Sender", "Message"]];

  for (const message of jsonData.messages) {
    const text = processTextEntities(message.text).replace(/<[^>]*>/g, ""); // Strip HTML
    rows.push([message.date, message.from, text]);
  }

  return rows.map((row) => row.map((cell) => `"${cell}"`).join(",")).join("\n");
}
```

### Sentiment analysis integration[​](#sentiment-analysis-integration "Direct link to Sentiment analysis integration")

Analyze message sentiment:

```
function getChatHtml() {
  const jsonData = JSON.parse(entity.c_json_data || "{}");
  let html = "";

  for (const message of jsonData.messages) {
    const messageText = processTextEntities(message.text);
    const sentiment = analyzeSentiment(messageText);
    const sentimentColor =
      sentiment > 0 ? "#4caf50" : sentiment < 0 ? "#f44336" : "#9e9e9e";

    html += `
      <div class="message" style="border-left: 3px solid ${sentimentColor}">
        <!-- message content -->
      </div>
    `;
  }

  return html;
}

function analyzeSentiment(text: string): number {
  // Simple keyword-based sentiment (replace with API call for accuracy)
  const positiveWords = [
    "thank",
    "great",
    "excellent",
    "appreciate",
    "perfect",
  ];
  const negativeWords = ["problem", "issue", "error", "failed", "wrong"];

  let score = 0;
  const lowerText = text.toLowerCase();

  positiveWords.forEach((word) => {
    if (lowerText.includes(word)) score++;
  });

  negativeWords.forEach((word) => {
    if (lowerText.includes(word)) score--;
  });

  return score;
}
```

tip

For more sophisticated sentiment analysis using AI reasoning instead of simple keyword matching, see [How to create an AI-based action](/developer-guide/how-to/advanced/how-to-create-ai-based-action.md). You can use OpenAI or other AI services to analyze message tone, emotion, and intent with much greater accuracy.

## Best practices[​](#best-practices "Direct link to Best practices")

1. **Validate JSON structure**: always check for required fields before accessing them
2. **Sanitize user content**: be cautious with user-generated HTML content
3. **Handle edge cases**: empty messages, missing timestamps, null values
4. **Optimize for readability**: use consistent spacing, colors, and typography
5. **Test with real data**: use actual Telegram exports to ensure compatibility
6. **Consider time zones**: convert timestamps to user's local timezone
7. **Implement error boundaries**: gracefully handle parsing errors
8. **Document data format**: keep notes on the expected JSON structure

## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting")

### Messages not displaying[​](#messages-not-displaying "Direct link to Messages not displaying")

Check if JSON is valid:

```
try {
  const jsonData = JSON.parse(entity.c_json_data);
  console.log("Parsed successfully:", jsonData);
} catch (e) {
  console.error("JSON parsing error:", e);
}
```

### Styling not applied[​](#styling-not-applied "Direct link to Styling not applied")

Ensure `<style jsx>` is inside the layout component and verify CSS class names match between styles and HTML.

### Text entities not formatting[​](#text-entities-not-formatting "Direct link to Text entities not formatting")

Add logging to debug:

```
function processTextEntities(text: any) {
  debugger;
  console.log("Processing text:", text);
  // ... rest of function
}
```

### Timestamps showing incorrectly[​](#timestamps-showing-incorrectly "Direct link to Timestamps showing incorrectly")

Verify the date format in your JSON and adjust the moment parsing:

```
moment(message.date, "YYYY-MM-DDTHH:mm:ss").format("...");
```

## Sample[​](#sample "Direct link to Sample")

Here's a complete example showing the formatted chat display:

![Formatted Telegram chat conversation displayed in a styled card layout with message bubbles, sender names, timestamps, and reply threading](/assets/images/crm-chat-telegram-8f39a1050808671e2fac831442f840f4.png)

Below is the full sample Telegram chat JSON that produces the above display:

```
{
  "name": "MobileCarrier Support",
  "type": "personal_chat",
  "id": 987654321,
  "messages": [
    {
      "id": 1,
      "type": "message",
      "date": "2024-10-08T14:23:15",
      "date_unixtime": "1728396195",
      "from": "John Miller",
      "from_id": "user445566",
      "text": "Hi, I just received my bill and it's $95 higher than usual. I was charged for international calls but I haven't made any calls abroad this month. Can you help me figure out what happened?",
      "text_entities": [
        {
          "type": "plain",
          "text": "Hi, I just received my bill and it's $95 higher than usual. I was charged for international calls but I haven't made any calls abroad this month. Can you help me figure out what happened?"
        }
      ]
    },
    {
      "id": 2,
      "type": "message",
      "date": "2024-10-08T14:24:32",
      "date_unixtime": "1728396272",
      "from": "Sarah - Support Agent",
      "from_id": "user778899",
      "reply_to_message_id": 1,
      "text": "Hello John! I'm sorry to hear about the billing issue. I'd be happy to help you resolve this. Could you please provide me with your account number or the phone number associated with the account? I'll take a look at the charges right away.",
      "text_entities": [
        {
          "type": "plain",
          "text": "Hello John! I'm sorry to hear about the billing issue. I'd be happy to help you resolve this. Could you please provide me with your account number or the phone number associated with the account? I'll take a look at the charges right away."
        }
      ]
    },
    {
      "id": 3,
      "type": "message",
      "date": "2024-10-08T14:25:48",
      "date_unixtime": "1728396348",
      "from": "John Miller",
      "from_id": "user445566",
      "text": "Sure, my phone number is (555) 123-4567. The charges are listed as \"International Calls - Zone 3\" totaling $94.87. I definitely didn't call any international numbers.",
      "text_entities": [
        {
          "type": "plain",
          "text": "Sure, my phone number is (555) 123-4567. The charges are listed as \"International Calls - Zone 3\" totaling $94.87. I definitely didn't call any international numbers."
        }
      ]
    },
    {
      "id": 4,
      "type": "message",
      "date": "2024-10-08T14:28:15",
      "date_unixtime": "1728396495",
      "from": "Sarah - Support Agent",
      "from_id": "user778899",
      "text": [
        "Thank you for that information. I've reviewed your account and I see the issue. You received calls from a number with country code +1-876 (Jamaica), which is actually considered international despite being +1. These were ",
        {
          "type": "bold",
          "text": "incoming calls"
        },
        " that you answered, totaling 47 minutes. I can see this was likely unexpected. I'm going to credit your account the full $94.87 since this wasn't clearly communicated. The credit will appear in 1-2 business days. 👍"
      ],
      "text_entities": [
        {
          "type": "plain",
          "text": "Thank you for that information. I've reviewed your account and I see the issue. You received calls from a number with country code +1-876 (Jamaica), which is actually considered international despite being +1. These were "
        },
        {
          "type": "bold",
          "text": "incoming calls"
        },
        {
          "type": "plain",
          "text": " that you answered, totaling 47 minutes. I can see this was likely unexpected. I'm going to credit your account the full $94.87 since this wasn't clearly communicated. The credit will appear in 1-2 business days. 👍"
        }
      ]
    },
    {
      "id": 5,
      "type": "message",
      "date": "2024-10-08T14:29:42",
      "date_unixtime": "1728396582",
      "from": "John Miller",
      "from_id": "user445566",
      "reply_to_message_id": 4,
      "text": "Oh wow, I had no idea! That makes sense now - my cousin called me from vacation. Thank you so much for the quick help and the credit, Sarah. Really appreciate it! 🙏",
      "text_entities": [
        {
          "type": "plain",
          "text": "Oh wow, I had no idea! That makes sense now - my cousin called me from vacation. Thank you so much for the quick help and the credit, Sarah. Really appreciate it! 🙏"
        }
      ]
    }
  ]
}
```

This sample demonstrates:

* **Multiple messages**: conversation flow between customer and support agent
* **Reply threading**: messages 2 and 5 reference previous messages
* **Text formatting**: bold text used to emphasize "incoming calls"
* **Mixed text formats**: both plain strings and array-based text entities
* **Timestamps**: ISO 8601 format dates that get formatted with moment.js
* **Real-world scenario**: customer support conversation about billing issue
