Skip to main content
Glama
ChatMessages.js21.7 kB
export default { name: "ChatMessages", data() { return { jid: "", messages: [], loading: false, searchQuery: "", startTime: "", endTime: "", isFromMe: "", onlyMedia: false, currentPage: 1, pageSize: 20, totalMessages: 0, // Media download tracking downloadedMedia: {}, // messageId -> { file_path, media_type, file_size, status } downloadingMedia: new Set(), // Set of messageIds currently downloading mediaDownloadErrors: {}, // messageId -> error message maxConcurrentDownloads: 3, currentDownloads: 0, }; }, computed: { totalPages() { return Math.ceil(this.totalMessages / this.pageSize); }, formattedJid() { return ( this.jid.trim() + (this.jid.includes("@") ? "" : "@s.whatsapp.net") ); }, }, methods: { isValidForm() { return this.jid.trim().length > 0; }, openModal() { // Check if there's a pre-selected JID from chat list const selectedJid = localStorage.getItem("selectedChatJid"); if (selectedJid) { this.jid = selectedJid; localStorage.removeItem("selectedChatJid"); // Clean up this.loadMessages(); } $("#modalChatMessages") .modal({ onShow: function () { // Initialize accordion after modal is shown setTimeout(() => { $("#modalChatMessages .ui.accordion").accordion(); }, 100); }, }) .modal("show"); }, async loadMessages() { if (!this.isValidForm()) { showErrorInfo("Please enter a valid JID"); return; } this.loading = true; try { const params = new URLSearchParams({ offset: (this.currentPage - 1) * this.pageSize, limit: this.pageSize, }); if (this.searchQuery.trim()) { params.append("search", this.searchQuery); } if (this.startTime) { params.append("start_time", this.startTime); } if (this.endTime) { params.append("end_time", this.endTime); } if (this.isFromMe !== "") { params.append("is_from_me", this.isFromMe); } if (this.onlyMedia) { params.append("media_only", "true"); } const response = await window.http.get( `/chat/${this.formattedJid}/messages?${params}` ); this.messages = response.data.results?.data || []; this.totalMessages = response.data.results?.pagination?.total || 0; if (this.messages.length === 0) { showErrorInfo("No messages found for the specified criteria"); } else { // Auto-download media for loaded messages this.downloadAllMediaInMessages(); } } catch (error) { showErrorInfo( error.response?.data?.message || "Failed to load messages" ); } finally { this.loading = false; } }, searchMessages() { this.currentPage = 1; this.loadMessages(); }, nextPage() { if (this.currentPage < this.totalPages) { this.currentPage++; this.loadMessages(); } }, prevPage() { if (this.currentPage > 1) { this.currentPage--; this.loadMessages(); } }, handleReset() { this.jid = ""; this.messages = []; this.searchQuery = ""; this.startTime = ""; this.endTime = ""; this.isFromMe = ""; this.onlyMedia = false; this.currentPage = 1; this.totalMessages = 0; // Clear media download state this.downloadedMedia = {}; this.downloadingMedia.clear(); this.mediaDownloadErrors = {}; this.currentDownloads = 0; }, formatTimestamp(timestamp) { if (!timestamp) return "N/A"; return moment(timestamp).format("MMM DD, YYYY HH:mm:ss"); }, formatMessageType(message) { if (message.media_type) return message.media_type.toUpperCase(); if (message.message_type) return message.message_type.toUpperCase(); return "TEXT"; }, formatSender(message) { if (message.is_from_me) return "Me"; return message.push_name || message.sender_jid || "Unknown"; }, getMessageContent(message) { if (message.content) return message.content; if (message.text) return message.text; if (message.caption) return message.caption; if (message.media_type) return `[${message.media_type.toUpperCase()}]`; return "[No content]"; }, getMediaDisplay(message) { if (!message.media_type || !message.url || !message.id) { return null; } const messageId = message.id; const downloadedInfo = this.downloadedMedia[messageId]; const isDownloaded = this.isMediaDownloaded(messageId); const isDownloading = this.isMediaDownloading(messageId); const hasError = this.hasMediaDownloadError(messageId); // Show loading state if (isDownloading) { return { type: 'loading', content: `<div class="ui active mini inline loader"></div> Downloading ${message.media_type}...` }; } // Show error state with retry option if (hasError) { return { type: 'error', content: `<div class="ui red message"> <i class="exclamation triangle icon"></i> Failed to download ${message.media_type} <span class="ui mini button" style="cursor: pointer; margin-left: 10px;" onclick="document.dispatchEvent(new CustomEvent('retryMediaDownload', {detail: '${messageId}'}))"> <i class="redo icon"></i> Retry </span> </div>` }; } // Show downloaded media if (isDownloaded && downloadedInfo) { const filePath = downloadedInfo.file_path; const mediaType = downloadedInfo.media_type; const filename = downloadedInfo.filename; const fileSize = downloadedInfo.file_size; switch (mediaType.toLowerCase()) { case 'image': return { type: 'image', content: `<div class="ui fluid image"> <img src="${filePath}" alt="${filename}" style="max-width: 300px; max-height: 300px; border-radius: 4px;" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"> <div style="display: none;" class="ui placeholder segment"> <div class="ui icon header"> <i class="image outline icon"></i> Image not available </div> </div> </div>` }; case 'video': return { type: 'video', content: `<div class="ui fluid"> <video controls style="max-width: 300px; max-height: 300px; border-radius: 4px;" preload="metadata"> <source src="${filePath}" type="video/mp4"> <source src="${filePath}" type="video/webm"> <source src="${filePath}" type="video/ogg"> Your browser does not support the video tag. </video> </div>` }; case 'audio': return { type: 'audio', content: `<div class="ui fluid"> <audio controls style="width: 100%; max-width: 300px;"> <source src="${filePath}" type="audio/mpeg"> <source src="${filePath}" type="audio/ogg"> <source src="${filePath}" type="audio/wav"> Your browser does not support the audio tag. </audio> </div>` }; case 'document': const sizeText = fileSize ? `(${Math.round(fileSize / 1024)} KB)` : ''; return { type: 'document', content: `<div class="ui labeled button"> <a href="${filePath}" download="${filename}" class="ui button"> <i class="download icon"></i> ${filename} ${sizeText} </a> <div class="ui basic left pointing label"> Document </div> </div>` }; case 'sticker': return { type: 'sticker', content: `<div class="ui"> <img src="${filePath}" alt="Sticker" style="max-width: 150px; max-height: 150px; border-radius: 4px;" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"> <div style="display: none;" class="ui placeholder segment"> <div class="ui icon header"> <i class="smile outline icon"></i> Sticker not available </div> </div> </div>` }; default: return { type: 'unknown', content: `<div class="ui message"> <i class="file icon"></i> Unknown media type: ${mediaType} </div>` }; } } // Default: show media available label return { type: 'available', content: `<div class="ui tiny blue label"> <i class="linkify icon"></i> ${message.media_type.toUpperCase()} Available </div>` }; }, getMessageStyle(message) { const baseStyle = { padding: "1em", margin: "0.5em 0", }; if (message.is_from_me) { return { ...baseStyle, borderLeft: "4px solid #2185d0", backgroundColor: "#f8f9fa", }; } else { return { ...baseStyle, borderLeft: "4px solid #767676", }; } }, // Media download methods isMediaDownloaded(messageId) { return this.downloadedMedia[messageId] && this.downloadedMedia[messageId].status === 'completed'; }, isMediaDownloading(messageId) { return this.downloadingMedia.has(messageId); }, hasMediaDownloadError(messageId) { return !!this.mediaDownloadErrors[messageId]; }, async downloadMediaForMessage(message) { if (!message.media_type || !message.url || !message.id) { return; } const messageId = message.id; // Skip if already downloaded or downloading if (this.isMediaDownloaded(messageId) || this.isMediaDownloading(messageId)) { return; } // Check concurrent download limit if (this.currentDownloads >= this.maxConcurrentDownloads) { return; } try { this.downloadingMedia.add(messageId); this.currentDownloads++; // Clear any previous error if (this.mediaDownloadErrors[messageId]) { delete this.mediaDownloadErrors[messageId]; } const response = await window.http.get( `/message/${messageId}/download?phone=${this.formattedJid}` ); if (response.data && response.data.results) { this.downloadedMedia[messageId] = { file_path: response.data.results.file_path, media_type: response.data.results.media_type, file_size: response.data.results.file_size, filename: response.data.results.filename, status: 'completed' }; } } catch (error) { console.error(`Failed to download media for message ${messageId}:`, error); this.mediaDownloadErrors[messageId] = error.response?.data?.message || 'Download failed'; } finally { this.downloadingMedia.delete(messageId); this.currentDownloads--; } }, async retryMediaDownload(messageId) { const message = this.messages.find(m => m.id === messageId); if (message) { // Clear the error first delete this.mediaDownloadErrors[messageId]; await this.downloadMediaForMessage(message); } }, async downloadAllMediaInMessages() { const mediaMessages = this.messages.filter(message => message.media_type && message.url && message.id && !this.isMediaDownloaded(message.id) && !this.isMediaDownloading(message.id) ); if (mediaMessages.length === 0) { return; } // Download in batches to respect concurrency limit const downloadQueue = [...mediaMessages]; const processQueue = async () => { while (downloadQueue.length > 0 && this.currentDownloads < this.maxConcurrentDownloads) { const message = downloadQueue.shift(); if (message) { await this.downloadMediaForMessage(message); // Small delay to prevent overwhelming the server await new Promise(resolve => setTimeout(resolve, 100)); } } // If there are still items in queue and we can download more, continue if (downloadQueue.length > 0 && this.currentDownloads < this.maxConcurrentDownloads) { setTimeout(processQueue, 500); // Wait a bit before checking again } }; // Start processing processQueue(); }, }, mounted() { // Store the event handler so we can remove it later this.handleOpenChatMessages = () => { this.openModal(); }; // Handle retry media download events this.handleRetryMediaDownload = (event) => { const messageId = event.detail; this.retryMediaDownload(messageId); }; // Listen for custom event from ChatList to open modal properly window.addEventListener('openChatMessages', this.handleOpenChatMessages); // Listen for retry media download events document.addEventListener('retryMediaDownload', this.handleRetryMediaDownload); }, beforeUnmount() { // Clean up event listeners if (this.handleOpenChatMessages) { window.removeEventListener('openChatMessages', this.handleOpenChatMessages); } if (this.handleRetryMediaDownload) { document.removeEventListener('retryMediaDownload', this.handleRetryMediaDownload); } }, template: ` <div class="purple card" @click="openModal()" style="cursor: pointer"> <div class="content"> <a class="ui purple right ribbon label">Chat</a> <div class="header">Chat Messages</div> <div class="description"> View messages from specific chats with advanced filtering </div> </div> </div> <!-- Modal ChatMessages --> <div class="ui large modal" id="modalChatMessages"> <i class="close icon"></i> <div class="header"> <i class="comment icon"></i> Chat Messages </div> <div class="content"> <div class="ui form"> <div class="field"> <label>Chat JID</label> <input type="text" placeholder="Enter phone number or full JID (e.g. 1234567890 or group-id@g.us)" v-model="jid"> </div> <div class="ui accordion"> <div class="title"> <i class="dropdown icon"></i> Advanced Filters (Optional) </div> <div class="content"> <div class="fields"> <div class="eight wide field"> <label>Search Message Content</label> <input type="text" placeholder="Search in message text..." v-model="searchQuery"> </div> <div class="four wide field"> <label>Sender Filter</label> <select class="ui dropdown" v-model="isFromMe"> <option value="">All messages</option> <option value="true">My messages</option> <option value="false">Their messages</option> </select> </div> <div class="four wide field"> <label>&nbsp;</label> <div class="ui checkbox"> <input type="checkbox" v-model="onlyMedia"> <label>Media only</label> </div> </div> </div> <div class="fields"> <div class="eight wide field"> <label>Start Date/Time</label> <input type="datetime-local" v-model="startTime"> </div> <div class="eight wide field"> <label>End Date/Time</label> <input type="datetime-local" v-model="endTime"> </div> </div> </div> </div> </div> <div class="ui divider"></div> <div class="actions"> <button class="ui primary button" :class="{'disabled': !isValidForm() || loading}" @click="loadMessages"> <i class="search icon"></i> {{ loading ? 'Loading...' : 'Load Messages' }} </button> <button class="ui button" @click="handleReset"> <i class="refresh icon"></i> Reset </button> </div> <div v-if="loading" class="ui active centered inline loader"></div> <div v-else-if="messages.length === 0 && totalMessages === 0" class="ui placeholder segment"> <div class="ui icon header"> <i class="comment outline icon"></i> No messages loaded </div> <p>Enter a JID and click "Load Messages" to view chat history</p> </div> <div v-else-if="messages.length > 0"> <div style="padding-top: 1em; padding-bottom: 1em;"> <div class="ui info message"> <div class="header"> Chat Messages for {{ formattedJid }} </div> <p>Showing {{ messages.length }} of {{ totalMessages }} messages</p> </div> </div> <div class="ui divided items" style="max-height: 400px; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; scrollbar-width: thin;"> <div v-for="message in messages" :key="message.id" class="item" :style="getMessageStyle(message)"> <div class="content"> <div class="header"> <div class="ui horizontal label" :class="message.is_from_me ? 'blue' : 'grey'"> {{ formatSender(message) }} </div> <div class="ui right floated horizontal label"> {{ formatMessageType(message) }} </div> </div> <div class="meta"> <span>{{ formatTimestamp(message.timestamp) }}</span> <span v-if="message.id" class="right floated"> ID: {{ message.id }} </span> </div> <div class="description"> <p>{{ getMessageContent(message) }}</p> <div v-if="message.media_type && message.url" class="media-container" style="margin-top: 0.5em;"> <div v-if="getMediaDisplay(message)" v-html="getMediaDisplay(message).content"></div> </div> </div> </div> </div> </div> <!-- Pagination --> <div class="ui pagination menu" v-if="totalPages > 1"> <a class="icon item" @click="prevPage" :class="{ disabled: currentPage === 1 }"> <i class="left chevron icon"></i> </a> <div class="item"> Page {{ currentPage }} of {{ totalPages }} </div> <a class="icon item" @click="nextPage" :class="{ disabled: currentPage === totalPages }"> <i class="right chevron icon"></i> </a> </div> </div> </div> <div class="actions"> <div class="ui approve button">Close</div> </div> </div> `, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/samihalawa/whatsapp-go-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server