diff options
Diffstat (limited to 'client/jchat-client.js')
-rw-r--r-- | client/jchat-client.js | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/client/jchat-client.js b/client/jchat-client.js new file mode 100644 index 0000000..4ab5428 --- /dev/null +++ b/client/jchat-client.js @@ -0,0 +1,352 @@ +/** + * 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 = ` + <div class="conversation-title">${conv.title || 'Untitled Conversation'}</div> + <div class="conversation-preview"> + ${conv.lastMessageAt ? `Last: ${new Date(conv.lastMessageAt).toLocaleTimeString()}` : 'No messages'} + </div> + `; + + container.appendChild(div); + }); + } + + renderMessages() { + const container = document.getElementById('messages'); + const messages = this.messages.get(this.currentConversationId) || []; + + if (messages.length === 0) { + container.innerHTML = ` + <div class="empty-state"> + <h3>No messages yet</h3> + <p>Start the conversation by sending a message</p> + </div> + `; + 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 = ` + <div class="message-header">${sentTime}</div> + <div class="message-body">${this.escapeHtml(msg.body)}</div> + `; + + 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(); + } +}; |