-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Modularize index.html script, localize markdown rendering
- Loading branch information
1 parent
e116981
commit 40a8f62
Showing
8 changed files
with
409 additions
and
304 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
export class ChatManager { | ||
constructor(websocketManager, uiManager) { | ||
this.websocketManager = websocketManager; | ||
this.uiManager = uiManager; | ||
this.typingTimeout = null; | ||
this.isTyping = false; | ||
|
||
this.setupEventListeners(); | ||
} | ||
|
||
setupEventListeners() { | ||
// Handle form submission | ||
this.uiManager.messageForm.addEventListener("submit", (e) => this.handleSubmit(e)); | ||
|
||
// Handle keydown for Enter key | ||
this.uiManager.messageInput.addEventListener("keydown", (e) => this.handleKeydown(e)); | ||
|
||
// Handle typing status | ||
this.uiManager.messageInput.addEventListener("input", () => this.handleTyping()); | ||
|
||
// Handle reconnect button | ||
this.uiManager.reconnectButton.addEventListener("click", () => { | ||
this.websocketManager.manualReconnect(); | ||
}); | ||
} | ||
|
||
handleSubmit(e) { | ||
e.preventDefault(); | ||
|
||
// Prevent sending if not connected | ||
if (!this.websocketManager.isConnectedStatus()) { | ||
return; | ||
} | ||
|
||
const content = this.uiManager.getMessageContent(); | ||
if (content) { | ||
this.uiManager.showSending(); | ||
|
||
const success = this.websocketManager.send({ | ||
type: "message", | ||
content, | ||
}); | ||
|
||
if (success) { | ||
this.uiManager.clearMessageInput(); | ||
} | ||
|
||
this.uiManager.hideSending(); | ||
} | ||
} | ||
|
||
handleKeydown(e) { | ||
if (e.key === "Enter" && !e.shiftKey) { | ||
// Prevent sending if not connected | ||
if (!this.websocketManager.isConnectedStatus()) { | ||
e.preventDefault(); | ||
return; | ||
} | ||
|
||
const content = this.uiManager.getMessageContent(); | ||
if (content) { | ||
e.preventDefault(); | ||
this.uiManager.messageForm.dispatchEvent(new Event("submit")); | ||
} | ||
} | ||
} | ||
|
||
handleTyping() { | ||
if (!this.isTyping) { | ||
this.isTyping = true; | ||
this.sendTypingStatus(true); | ||
} | ||
|
||
// Clear previous timeout | ||
if (this.typingTimeout) { | ||
clearTimeout(this.typingTimeout); | ||
} | ||
|
||
// Set new timeout | ||
this.typingTimeout = setTimeout(() => { | ||
this.isTyping = false; | ||
this.sendTypingStatus(false); | ||
}, 1000); | ||
} | ||
|
||
sendTypingStatus(isTyping) { | ||
this.websocketManager.send({ | ||
type: "typing", | ||
isTyping: isTyping | ||
}); | ||
} | ||
|
||
async handleInitialLoad() { | ||
try { | ||
const response = await fetch("/messages"); | ||
if (!response.ok) { | ||
throw new Error("Failed to load messages"); | ||
} | ||
const messages = await response.json(); | ||
messages.reverse(); | ||
|
||
messages.forEach((msg) => { | ||
this.uiManager.appendMessage(msg); | ||
}); | ||
this.uiManager.updateEmptyState(); | ||
} catch (error) { | ||
console.error("Error loading messages:", error); | ||
} finally { | ||
this.uiManager.hideLoadingState(); | ||
requestAnimationFrame(() => { | ||
this.uiManager.scrollToBottom(false); | ||
}); | ||
} | ||
} | ||
|
||
handleWebSocketMessage(event) { | ||
const data = JSON.parse(event.data); | ||
|
||
switch (data.type) { | ||
case "message": | ||
this.uiManager.appendMessage(data); | ||
break; | ||
|
||
case "typing": | ||
this.uiManager.updateTypingIndicator(data.message); | ||
break; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { WebSocketManager } from './websocket.js'; | ||
import { UIManager } from './ui.js'; | ||
import { ChatManager } from './chat.js'; | ||
|
||
document.addEventListener("DOMContentLoaded", () => { | ||
// Initialize managers | ||
const uiManager = new UIManager(); | ||
|
||
// Show initial loading state | ||
uiManager.showLoadingState(); | ||
|
||
// Initialize WebSocket manager with callbacks | ||
const websocketManager = new WebSocketManager( | ||
// onOpen callback | ||
async () => { | ||
await chatManager.handleInitialLoad(); | ||
}, | ||
// onMessage callback | ||
(event) => { | ||
chatManager.handleWebSocketMessage(event); | ||
}, | ||
// onConnectionStatusChange callback | ||
(status, message) => { | ||
uiManager.updateConnectionStatus(status, message); | ||
} | ||
); | ||
|
||
// Initialize chat manager | ||
const chatManager = new ChatManager(websocketManager, uiManager); | ||
|
||
// Start WebSocket connection | ||
websocketManager.connect(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
export class UIManager { | ||
constructor() { | ||
this.messagesElement = document.getElementById("messages"); | ||
this.messagesLoading = document.getElementById("messages-loading"); | ||
this.messageForm = document.getElementById("message-form"); | ||
this.messageInput = document.getElementById("message-input"); | ||
this.typingIndicator = document.getElementById("typing-indicator"); | ||
this.emptyState = document.getElementById("empty-state"); | ||
this.sendButton = this.messageForm.querySelector('button[type="submit"]'); | ||
this.sendText = this.sendButton.querySelector(".send-text"); | ||
this.sendingText = this.sendButton.querySelector(".sending-text"); | ||
this.reconnectButton = document.getElementById("reconnect-button"); | ||
this.connectionStatus = document.getElementById("connection-status"); | ||
this.connectionText = document.getElementById("connection-text"); | ||
|
||
this.setupTextareaAutoResize(); | ||
} | ||
|
||
updateConnectionStatus(status, message) { | ||
this.connectionText.textContent = message; | ||
this.reconnectButton.classList.toggle("hidden", status !== "disconnected"); | ||
|
||
// Disable/enable send button based on connection status | ||
this.sendButton.disabled = status !== "connected"; | ||
this.sendButton.classList.toggle("opacity-50", status !== "connected"); | ||
this.messageInput.disabled = status !== "connected"; | ||
|
||
switch (status) { | ||
case "connected": | ||
this.connectionStatus.className = "h-2 w-2 rounded-full bg-green-500"; | ||
this.connectionText.className = "text-sm text-green-600 dark:text-green-400"; | ||
break; | ||
case "connecting": | ||
this.connectionStatus.className = "h-2 w-2 rounded-full bg-yellow-500"; | ||
this.connectionText.className = "text-sm text-yellow-600 dark:text-yellow-400"; | ||
break; | ||
case "disconnected": | ||
this.connectionStatus.className = "h-2 w-2 rounded-full bg-red-500"; | ||
this.connectionText.className = "text-sm text-red-600 dark:text-red-400"; | ||
break; | ||
} | ||
} | ||
|
||
createMessageElement(msg) { | ||
const messageElement = document.createElement("div"); | ||
messageElement.className = | ||
"bg-gray-50 dark:bg-gray-700 rounded-lg p-4 transition-colors duration-200 mb-4 last:mb-0"; | ||
|
||
// Sanitize and render markdown | ||
const renderedContent = marked.parse(msg.content); | ||
|
||
messageElement.innerHTML = ` | ||
<div class="flex justify-between items-start"> | ||
<span class="font-medium text-blue-600 dark:text-blue-400">${msg.username}</span> | ||
<span class="text-xs text-gray-500 dark:text-gray-400">${new Date( | ||
msg.timestamp || new Date() | ||
).toLocaleTimeString()}</span> | ||
</div> | ||
<div class="mt-2 text-gray-800 dark:text-gray-200">${renderedContent}</div> | ||
`; | ||
return messageElement; | ||
} | ||
|
||
appendMessage(msg) { | ||
const messageElement = this.createMessageElement(msg); | ||
this.messagesElement.appendChild(messageElement); | ||
this.updateEmptyState(); | ||
if (this.isNearBottom()) { | ||
this.scrollToBottom(true); | ||
} | ||
} | ||
|
||
updateTypingIndicator(message) { | ||
this.typingIndicator.textContent = message; | ||
} | ||
|
||
isNearBottom() { | ||
const threshold = 150; // pixels from bottom | ||
return ( | ||
this.messagesElement.scrollHeight - | ||
this.messagesElement.scrollTop - | ||
this.messagesElement.clientHeight <= | ||
threshold | ||
); | ||
} | ||
|
||
scrollToBottom(smooth = false) { | ||
this.messagesElement.scrollTo({ | ||
top: this.messagesElement.scrollHeight, | ||
behavior: smooth ? "smooth" : "auto" | ||
}); | ||
} | ||
|
||
updateEmptyState() { | ||
if (this.messagesElement.children.length === 1) { | ||
// Only empty-state div present | ||
this.emptyState.classList.remove("hidden"); | ||
} else { | ||
this.emptyState.classList.add("hidden"); | ||
} | ||
} | ||
|
||
showLoadingState() { | ||
this.messagesElement.style.display = "none"; | ||
this.messagesLoading.style.display = "flex"; | ||
} | ||
|
||
hideLoadingState() { | ||
this.messagesLoading.style.display = "none"; | ||
this.messagesElement.style.display = "block"; | ||
} | ||
|
||
setupTextareaAutoResize() { | ||
this.messageInput.addEventListener("input", () => { | ||
// Reset height to auto to get proper scrollHeight | ||
this.messageInput.style.height = "auto"; | ||
// Set new height but cap it at max height | ||
const maxHeight = 150; // 150px max height | ||
const newHeight = Math.min(this.messageInput.scrollHeight + 2, maxHeight); | ||
this.messageInput.style.height = newHeight + "px"; | ||
}); | ||
} | ||
|
||
getMessageContent() { | ||
return this.messageInput.value.trim(); | ||
} | ||
|
||
clearMessageInput() { | ||
this.messageInput.value = ""; | ||
this.messageInput.style.height = "auto"; | ||
} | ||
|
||
showSending() { | ||
this.sendText.classList.add("hidden"); | ||
this.sendingText.classList.remove("hidden"); | ||
this.sendButton.disabled = true; | ||
} | ||
|
||
hideSending() { | ||
this.sendText.classList.remove("hidden"); | ||
this.sendingText.classList.add("hidden"); | ||
this.sendButton.disabled = false; | ||
} | ||
} |
Oops, something went wrong.