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
The process involves:
- storing the raw JSON chat data in a text field
- creating a custom layout with CSS styling for the chat interface
- parsing the JSON data in view logic
- formatting message text entities (bold, italic, links, etc.)
- handling message replies and threading
- rendering the formatted HTML
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
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
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
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
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
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
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
The processTextEntities()
function handles two formats:
- Simple string: returns as-is
- Array format: processes each element and applies formatting
Supported entity types:
bold
→<strong>
tagsitalic
→<em>
tagsunderline
→<u>
tagsstrikethrough
→<s>
tagscode
→ inline<code>
tagspre
→ code block<pre>
tagslink
→ clickable<a>
tagsmention
→ styled spans for @mentionshashtag
→ styled spans for #hashtags
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
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
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
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
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
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
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
Advanced scenarios
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
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;
}
For more sophisticated sentiment analysis using AI reasoning instead of simple keyword matching, see How to create an AI-based action. You can use OpenAI or other AI services to analyze message tone, emotion, and intent with much greater accuracy.
Best practices
- Validate JSON structure: always check for required fields before accessing them
- Sanitize user content: be cautious with user-generated HTML content
- Handle edge cases: empty messages, missing timestamps, null values
- Optimize for readability: use consistent spacing, colors, and typography
- Test with real data: use actual Telegram exports to ensure compatibility
- Consider time zones: convert timestamps to user's local timezone
- Implement error boundaries: gracefully handle parsing errors
- Document data format: keep notes on the expected JSON structure
Troubleshooting
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
Ensure <style jsx>
is inside the layout component and verify CSS class names match between styles and HTML.
Text entities not formatting
Add logging to debug:
function processTextEntities(text: any) {
debugger;
console.log("Processing text:", text);
// ... rest of function
}
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
Here's a complete example showing the formatted chat display:

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