/** * JCHAT Web Client - JMAP-based Chat Application */ class JChatClient { constructor() { this.serverUrl = JChatConfig.API_BASE_URL; this.session = null; this.conversations = new Map(); this.messages = new Map(); this.currentConversationId = null; this.userId = 'user1'; // Demo user this.init(); } async init() { try { await this.loadSession(); await this.loadConversations(); this.updateConnectionStatus('Connected', 'success'); // Set up polling for new messages (in production, use EventSource/WebSockets) this.startPolling(); } catch (error) { console.error('Failed to initialize client:', error); this.updateConnectionStatus('Connection failed', 'error'); } } async loadSession() { const response = await fetch(`${this.serverUrl}/jmap/session`); if (!response.ok) { throw new Error('Failed to load session'); } this.session = await response.json(); console.log('Session loaded:', this.session); } async makeJMAPRequest(methodCalls) { const request = { using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:chat'], methodCalls: methodCalls }; const response = await fetch(`${this.serverUrl}/jmap/api`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request) }); if (!response.ok) { throw new Error(`JMAP request failed: ${response.status}`); } return await response.json(); } async loadConversations() { try { // For demo, create a sample conversation if none exist const queryResponse = await this.makeJMAPRequest([ ['Conversation/query', { accountId: 'default' }, 'q1'] ]); const queryResult = queryResponse.methodResponses[0][1]; if (queryResult.ids.length === 0) { // Create a demo conversation await this.createDemoConversation(); return this.loadConversations(); } // Load conversation details const getResponse = await this.makeJMAPRequest([ ['Conversation/get', { accountId: 'default', ids: queryResult.ids }, 'g1'] ]); const conversations = getResponse.methodResponses[0][1].list; this.conversations.clear(); conversations.forEach(conv => { this.conversations.set(conv.id, conv); }); this.renderConversations(); } catch (error) { console.error('Failed to load conversations:', error); this.showStatus('Failed to load conversations', 'error'); } } async createDemoConversation() { try { const response = await this.makeJMAPRequest([ ['Conversation/set', { accountId: 'default', create: { 'demo1': { title: 'Demo Conversation', participantIds: [this.userId, 'user2'] } } }, 'c1'] ]); console.log('Demo conversation created:', response); } catch (error) { console.error('Failed to create demo conversation:', error); } } async loadMessages(conversationId) { try { // Query messages for the conversation const queryResponse = await this.makeJMAPRequest([ ['Message/query', { accountId: 'default', filter: { inConversation: conversationId }, sort: [{ property: 'sentAt', isAscending: true }] }, 'mq1'] ]); const messageIds = queryResponse.methodResponses[0][1].ids; if (messageIds.length === 0) { this.messages.set(conversationId, []); this.renderMessages(); return; } // Get message details const getResponse = await this.makeJMAPRequest([ ['Message/get', { accountId: 'default', ids: messageIds }, 'mg1'] ]); const messages = getResponse.methodResponses[0][1].list; this.messages.set(conversationId, messages); this.renderMessages(); } catch (error) { console.error('Failed to load messages:', error); this.showStatus('Failed to load messages', 'error'); } } renderConversations() { const container = document.getElementById('conversations'); container.innerHTML = ''; this.conversations.forEach(conv => { const div = document.createElement('div'); div.className = 'conversation'; div.onclick = () => this.selectConversation(conv.id); if (conv.id === this.currentConversationId) { div.classList.add('active'); } div.innerHTML = `
${conv.title || 'Untitled Conversation'}
${conv.lastMessageAt ? `Last: ${new Date(conv.lastMessageAt).toLocaleTimeString()}` : 'No messages'}
`; container.appendChild(div); }); } renderMessages() { const container = document.getElementById('messages'); const messages = this.messages.get(this.currentConversationId) || []; if (messages.length === 0) { container.innerHTML = `

No messages yet

Start the conversation by sending a message

`; return; } container.innerHTML = ''; messages.forEach(msg => { const div = document.createElement('div'); div.className = 'message'; if (msg.senderId === this.userId) { div.classList.add('sent'); } const sentTime = new Date(msg.sentAt).toLocaleTimeString(); div.innerHTML = `
${sentTime}
${this.escapeHtml(msg.body)}
`; container.appendChild(div); }); // Scroll to bottom container.scrollTop = container.scrollHeight; } async selectConversation(conversationId) { this.currentConversationId = conversationId; const conversation = this.conversations.get(conversationId); // Update UI document.getElementById('conversationTitle').textContent = conversation.title || 'Untitled Conversation'; document.getElementById('compose').style.display = 'flex'; // Re-render conversations to show selection this.renderConversations(); // Load and render messages await this.loadMessages(conversationId); } async sendMessage() { const input = document.getElementById('messageInput'); const message = input.value.trim(); if (!message || !this.currentConversationId) { return; } try { const response = await this.makeJMAPRequest([ ['Message/set', { accountId: 'default', create: { 'temp1': { conversationId: this.currentConversationId, body: message, senderId: this.userId } } }, 'm1'] ]); console.log('Message sent:', response); // Clear input input.value = ''; // Reload messages await this.loadMessages(this.currentConversationId); await this.loadConversations(); // Update conversation preview } catch (error) { console.error('Failed to send message:', error); this.showStatus('Failed to send message', 'error'); } } async createNewConversation() { const title = prompt('Enter conversation title:'); if (!title) return; try { const response = await this.makeJMAPRequest([ ['Conversation/set', { accountId: 'default', create: { 'new1': { title: title, participantIds: [this.userId] } } }, 'nc1'] ]); console.log('New conversation created:', response); await this.loadConversations(); } catch (error) { console.error('Failed to create conversation:', error); this.showStatus('Failed to create conversation', 'error'); } } startPolling() { // Simple polling for demo - in production use EventSource or WebSockets setInterval(async () => { if (this.currentConversationId) { await this.loadMessages(this.currentConversationId); } }, 5000); // Poll every 5 seconds } updateConnectionStatus(text, type) { const statusElement = document.getElementById('connectionStatus'); statusElement.textContent = text; statusElement.className = type; } showStatus(message, type) { // Remove existing status const existing = document.querySelector('.status'); if (existing) { existing.remove(); } // Create new status const status = document.createElement('div'); status.className = `status ${type}`; status.textContent = message; document.body.appendChild(status); // Auto-remove after 3 seconds setTimeout(() => { if (status.parentNode) { status.remove(); } }, 3000); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Global functions for HTML event handlers window.jchatClient = new JChatClient(); window.sendMessage = () => { window.jchatClient.sendMessage(); }; window.createNewConversation = () => { window.jchatClient.createNewConversation(); }; window.handleKeyPress = (event) => { if (event.key === 'Enter') { window.jchatClient.sendMessage(); } };