aboutsummaryrefslogtreecommitdiff
path: root/client/jchat-client.js
diff options
context:
space:
mode:
Diffstat (limited to 'client/jchat-client.js')
-rw-r--r--client/jchat-client.js352
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();
+ }
+};