diff --git a/handlers/app.go b/handlers/app.go
index 69abd92..ffc0881 100644
--- a/handlers/app.go
+++ b/handlers/app.go
@@ -18,6 +18,8 @@ type PageData struct {
BasePath string
StaticPath string
CurrentURL string
+ ContentTemplate string
+ BodyClass string
}
type Config struct {
diff --git a/handlers/chat.go b/handlers/chat.go
index 5f2bda3..5314cd7 100644
--- a/handlers/chat.go
+++ b/handlers/chat.go
@@ -238,6 +238,8 @@ func ChatHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.Path,
+ ContentTemplate: "chat-content",
+ BodyClass: "chat-page",
},
Board: *board,
Messages: messages,
diff --git a/static/chat.js b/static/chat.js
new file mode 100644
index 0000000..7bdba72
--- /dev/null
+++ b/static/chat.js
@@ -0,0 +1,564 @@
+(() => {
+ 'use strict';
+
+ const chatContainer = document.querySelector('.chat-container');
+ if (!chatContainer) {
+ return;
+ }
+
+ const boardId = chatContainer.dataset.boardId;
+ const basePath = chatContainer.dataset.basePath || '';
+ const currentUsername = chatContainer.dataset.currentUsername || '';
+ const usernamesScript = document.getElementById('chat-usernames');
+ let allUsernames = [];
+ if (usernamesScript) {
+ try {
+ allUsernames = JSON.parse(usernamesScript.textContent || '[]');
+ } catch (err) {
+ console.error('Failed to parse chat usernames:', err);
+ allUsernames = [];
+ }
+ }
+
+ let ws;
+ let autocompleteActive = false;
+ let replyToId = -1;
+ let reconnectAttempts = 0;
+ let reconnectTimeout;
+ let isUserAtBottom = true;
+ let unreadCount = 0;
+ const maxReconnectDelay = 30000;
+ const typingUsers = new Set();
+
+ function updateConnectionStatus(status) {
+ const dot = document.getElementById('connection-dot');
+ const text = document.getElementById('connection-text');
+ if (!dot || !text) {
+ return;
+ }
+ dot.className = 'connection-dot ' + status;
+
+ if (status === 'connected') {
+ text.textContent = 'Connected';
+ reconnectAttempts = 0;
+ } else if (status === 'connecting') {
+ text.textContent = 'Connecting...';
+ } else if (status === 'disconnected') {
+ text.textContent = 'Disconnected';
+ }
+ }
+
+ function connectWebSocket() {
+ if (!boardId) {
+ return;
+ }
+ updateConnectionStatus('connecting');
+
+ ws = new WebSocket('ws://' + window.location.host + basePath + '/chat/?ws=true&id=' + boardId);
+
+ ws.onopen = function() {
+ updateConnectionStatus('connected');
+ reconnectAttempts = 0;
+ };
+
+ ws.onmessage = function(event) {
+ const msg = JSON.parse(event.data);
+
+ if (msg.type === 'typing') {
+ showTypingIndicator(msg.username);
+ return;
+ }
+
+ appendMessage(msg);
+ };
+
+ ws.onclose = function() {
+ updateConnectionStatus('disconnected');
+ console.log('WebSocket closed, reconnecting...');
+ scheduleReconnect();
+ };
+
+ ws.onerror = function(error) {
+ console.error('WebSocket error:', error);
+ updateConnectionStatus('disconnected');
+ };
+ }
+
+ function scheduleReconnect() {
+ if (reconnectTimeout) {
+ clearTimeout(reconnectTimeout);
+ }
+
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), maxReconnectDelay);
+ reconnectAttempts++;
+
+ console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts})...`);
+ reconnectTimeout = setTimeout(connectWebSocket, delay);
+ }
+
+ function sendMessage() {
+ const input = document.getElementById('chat-input-text');
+ if (!input) {
+ return;
+ }
+ const content = input.value.trim();
+ if (content === '') {
+ return;
+ }
+
+ const msg = {
+ type: 'message',
+ content: content,
+ replyTo: replyToId
+ };
+
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify(msg));
+ input.value = '';
+ cancelReply();
+ } else {
+ console.error('WebSocket is not open. Current state:', ws ? ws.readyState : 'undefined');
+ alert('Cannot send message: Not connected to chat server');
+ }
+ }
+
+ function sendTypingIndicator() {
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type: 'typing', username: currentUsername }));
+ }
+ }
+
+ function showTypingIndicator(username) {
+ if (username === currentUsername) {
+ return;
+ }
+
+ typingUsers.add(username);
+ updateTypingDisplay();
+
+ setTimeout(() => {
+ typingUsers.delete(username);
+ updateTypingDisplay();
+ }, 3000);
+ }
+
+ function updateTypingDisplay() {
+ const indicator = document.getElementById('typing-indicator');
+ const usersSpan = document.getElementById('typing-users');
+
+ if (!indicator || !usersSpan) {
+ return;
+ }
+
+ if (typingUsers.size === 0) {
+ indicator.classList.remove('visible');
+ } else {
+ const users = Array.from(typingUsers);
+ if (users.length === 1) {
+ usersSpan.textContent = `${users[0]} is typing`;
+ } else if (users.length === 2) {
+ usersSpan.textContent = `${users[0]} and ${users[1]} are typing`;
+ } else {
+ usersSpan.textContent = `${users[0]}, ${users[1]}, and ${users.length - 2} other${users.length - 2 > 1 ? 's' : ''} are typing`;
+ }
+ indicator.classList.add('visible');
+ }
+ }
+
+ function appendMessage(msg) {
+ const messages = document.getElementById('chat-messages');
+ if (!messages) {
+ return;
+ }
+
+ if (document.getElementById('msg-' + msg.id)) {
+ return;
+ }
+
+ const wasAtBottom = isUserAtBottom;
+
+ const msgDiv = document.createElement('div');
+ let highlightClass = '';
+ if (msg.mentions && msg.mentions.includes(currentUsername)) {
+ highlightClass = ' chat-message-highlighted';
+ }
+ msgDiv.className = 'chat-message' + highlightClass;
+ msgDiv.id = 'msg-' + msg.id;
+ msgDiv.dataset.user = msg.username;
+ msgDiv.dataset.reply = msg.replyTo || 0;
+
+ let pfpHTML = '
';
+ if (msg.pfpFileId && msg.pfpFileId.Valid) {
+ pfpHTML = `
`;
+ }
+ const replyHTML = msg.replyTo > 0 ? `Replying to message...
` : '';
+
+ msgDiv.innerHTML = `
+
+ ${replyHTML}
+ ${msg.content}
+
+ `;
+ messages.appendChild(msgDiv);
+
+ applyGrouping();
+
+ if (wasAtBottom) {
+ messages.scrollTop = messages.scrollHeight;
+ unreadCount = 0;
+ updateUnreadBadge();
+ } else if (msg.username !== currentUsername) {
+ unreadCount++;
+ updateUnreadBadge();
+ }
+ }
+
+ function updateUnreadBadge() {
+ const badge = document.getElementById('unread-badge');
+ if (!badge) {
+ return;
+ }
+ if (unreadCount > 0) {
+ badge.textContent = unreadCount > 99 ? '99+' : unreadCount;
+ badge.style.display = 'flex';
+ } else {
+ badge.style.display = 'none';
+ }
+ }
+
+ function jumpToBottom() {
+ const messages = document.getElementById('chat-messages');
+ if (!messages) {
+ return;
+ }
+ messages.scrollTo({
+ top: messages.scrollHeight,
+ behavior: 'smooth'
+ });
+ unreadCount = 0;
+ updateUnreadBadge();
+ }
+
+ function checkScrollPosition() {
+ const messages = document.getElementById('chat-messages');
+ const jumpButton = document.getElementById('jump-to-bottom');
+ if (!messages || !jumpButton) {
+ return;
+ }
+ const threshold = 100;
+
+ isUserAtBottom = messages.scrollHeight - messages.scrollTop - messages.clientHeight < threshold;
+
+ if (isUserAtBottom) {
+ jumpButton.classList.remove('visible');
+ unreadCount = 0;
+ updateUnreadBadge();
+ } else {
+ jumpButton.classList.add('visible');
+ }
+ }
+
+ function replyToMessage(id, username) {
+ replyToId = id;
+ const replyIndicator = document.getElementById('reply-indicator');
+ const replyUsernameSpan = document.getElementById('reply-username');
+ if (!replyIndicator || !replyUsernameSpan) {
+ return;
+ }
+ replyUsernameSpan.textContent = `Replying to ${username}`;
+ replyIndicator.style.display = 'flex';
+ const input = document.getElementById('chat-input-text');
+ if (input) {
+ input.focus();
+ }
+ }
+
+ function cancelReply() {
+ replyToId = -1;
+ const replyIndicator = document.getElementById('reply-indicator');
+ if (replyIndicator) {
+ replyIndicator.style.display = 'none';
+ }
+ }
+
+ function scrollToMessage(id) {
+ const msgElement = document.getElementById('msg-' + id);
+ if (msgElement) {
+ msgElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ msgElement.style.transition = 'background-color 0.5s';
+ msgElement.style.backgroundColor = '#f582ae';
+ setTimeout(() => {
+ msgElement.style.backgroundColor = '';
+ }, 1000);
+ }
+ }
+
+ function showAutocompletePopup(suggestions, x, y) {
+ const popup = document.getElementById('autocomplete-popup');
+ if (!popup) {
+ return;
+ }
+ popup.innerHTML = '';
+ popup.style.position = 'fixed';
+ popup.style.left = x + 'px';
+ popup.style.top = y + 'px';
+ popup.style.display = 'block';
+ autocompleteActive = true;
+ suggestions.forEach(username => {
+ const item = document.createElement('div');
+ item.className = 'autocomplete-item';
+ item.textContent = username;
+ item.onmousedown = (e) => {
+ e.preventDefault();
+ completeMention(username);
+ popup.style.display = 'none';
+ autocompleteActive = false;
+ };
+ popup.appendChild(item);
+ });
+ }
+
+ function completeMention(username) {
+ const input = document.getElementById('chat-input-text');
+ if (!input) {
+ return;
+ }
+ const text = input.value;
+ const caretPos = input.selectionStart;
+ const textBeforeCaret = text.substring(0, caretPos);
+ const atIndex = textBeforeCaret.lastIndexOf('@');
+
+ if (atIndex !== -1) {
+ const prefix = text.substring(0, atIndex);
+ const suffix = text.substring(caretPos);
+ input.value = prefix + '@' + username + ' ' + suffix;
+ const newCaretPos = prefix.length + 1 + username.length + 1;
+ input.focus();
+ input.setSelectionRange(newCaretPos, newCaretPos);
+ }
+ }
+
+ function getCaretCoordinates(element, position) {
+ const mirrorDivId = 'input-mirror-div';
+ const div = document.createElement('div');
+ div.id = mirrorDivId;
+ document.body.appendChild(div);
+ const style = window.getComputedStyle(element);
+ const properties = ['border', 'boxSizing', 'fontFamily', 'fontSize', 'fontWeight', 'letterSpacing', 'lineHeight', 'padding', 'textDecoration', 'textIndent', 'textTransform', 'whiteSpace', 'wordSpacing', 'wordWrap', 'width'];
+ properties.forEach(prop => {
+ div.style[prop] = style[prop];
+ });
+ div.style.position = 'absolute';
+ div.style.top = '-9999px';
+ div.style.left = '0px';
+ div.textContent = element.value.substring(0, position);
+
+ const span = document.createElement('span');
+ span.textContent = element.value.substring(position) || '.';
+ div.appendChild(span);
+
+ const coords = { top: span.offsetTop, left: span.offsetLeft };
+ document.body.removeChild(div);
+ return coords;
+ }
+
+ function applyGrouping() {
+ const container = document.getElementById('chat-messages');
+ if (!container) {
+ return;
+ }
+ const msgs = Array.from(container.querySelectorAll('.chat-message')).filter(el => el.id.startsWith('msg-'));
+
+ for (let i = 0; i < msgs.length; i++) {
+ const curr = msgs[i];
+
+ curr.classList.remove('grouped');
+
+ if (i === 0) {
+ continue;
+ }
+
+ const prev = msgs[i - 1];
+ const currUser = curr.dataset.user;
+ const prevUser = prev.dataset.user;
+ const currReply = parseInt(curr.dataset.reply, 10) || -1;
+
+ if (currUser === prevUser && (currReply <= 0)) {
+ curr.classList.add('grouped');
+ }
+ }
+ }
+
+ function handleAutocompleteInput(e) {
+ const input = e.target;
+ const text = input.value;
+ const caretPos = input.selectionStart;
+ const popup = document.getElementById('autocomplete-popup');
+ if (!popup) {
+ return;
+ }
+ const textBeforeCaret = text.substring(0, caretPos);
+ const atIndex = textBeforeCaret.lastIndexOf('@');
+
+ if (atIndex !== -1 && (atIndex === 0 || /\s/.test(text.charAt(atIndex - 1)))) {
+ const query = textBeforeCaret.substring(atIndex + 1);
+ if (!/\s/.test(query)) {
+ const suggestions = allUsernames.filter(u => u.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10);
+ if (suggestions.length > 0 && query.length > 0) {
+ const coords = getCaretCoordinates(input, atIndex);
+ const rect = input.getBoundingClientRect();
+ showAutocompletePopup(suggestions, rect.left + coords.left, rect.top + coords.top + 20);
+ } else {
+ popup.style.display = 'none';
+ autocompleteActive = false;
+ }
+ return;
+ }
+ }
+ popup.style.display = 'none';
+ autocompleteActive = false;
+ }
+
+ function handleAutocompleteBlur() {
+ setTimeout(() => {
+ if (!document.querySelector('.autocomplete-popup:hover')) {
+ const popup = document.getElementById('autocomplete-popup');
+ if (popup) {
+ popup.style.display = 'none';
+ }
+ autocompleteActive = false;
+ }
+ }, 150);
+ }
+
+ function handleMessageKeydown(e) {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage();
+ }
+ }
+
+ function setupDraftAutoSave(chatInput) {
+ if (!chatInput) {
+ return;
+ }
+ const draftKey = `draft_chat_${boardId}`;
+ let draftSaveTimeout;
+
+ const existingDraft = loadDraft(draftKey);
+ const draftTimestamp = getDraftTimestamp(draftKey);
+ if (existingDraft && draftTimestamp) {
+ const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
+ if (draftTimestamp > sevenDaysAgo) {
+ const indicator = showDraftIndicator(
+ existingDraft,
+ draftTimestamp,
+ (content) => {
+ chatInput.value = content;
+ chatInput.focus();
+ },
+ () => {
+ clearDraft(draftKey);
+ }
+ );
+ const chatInputContainer = document.querySelector('.chat-input');
+ if (chatInputContainer && chatInputContainer.parentNode) {
+ chatInputContainer.parentNode.insertBefore(indicator, chatInputContainer);
+ }
+ } else {
+ clearDraft(draftKey);
+ }
+ }
+
+ chatInput.addEventListener('input', () => {
+ clearTimeout(draftSaveTimeout);
+ draftSaveTimeout = setTimeout(() => {
+ const content = chatInput.value.trim();
+ if (content) {
+ saveDraft(draftKey, content);
+ } else {
+ clearDraft(draftKey);
+ }
+ }, 2000);
+ });
+ }
+
+ function setupTypingIndicator(chatInput) {
+ if (!chatInput) {
+ return;
+ }
+ let lastTypingTime = 0;
+ chatInput.addEventListener('input', () => {
+ const now = Date.now();
+ if (now - lastTypingTime > 2000) {
+ sendTypingIndicator();
+ lastTypingTime = now;
+ }
+ });
+ }
+
+ function setupSendMessageDraftClear(chatInput) {
+ if (!chatInput) {
+ return;
+ }
+ const draftKey = `draft_chat_${boardId}`;
+ const originalSendMessage = window.sendMessage;
+ window.sendMessage = function() {
+ const content = chatInput.value.trim();
+ if (content !== '') {
+ clearDraft(draftKey);
+ }
+ if (typeof originalSendMessage === 'function') {
+ originalSendMessage();
+ } else {
+ sendMessage();
+ }
+ };
+ }
+
+ function initChat() {
+ connectWebSocket();
+
+ const messagesContainer = document.getElementById('chat-messages');
+ if (messagesContainer) {
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+ applyGrouping();
+ messagesContainer.addEventListener('scroll', checkScrollPosition);
+ }
+
+ const jumpButton = document.getElementById('jump-to-bottom');
+ if (jumpButton) {
+ jumpButton.addEventListener('click', jumpToBottom);
+ }
+
+ const chatInput = document.getElementById('chat-input-text');
+ if (chatInput) {
+ chatInput.addEventListener('input', handleAutocompleteInput);
+ chatInput.addEventListener('blur', handleAutocompleteBlur);
+ chatInput.addEventListener('keydown', handleMessageKeydown);
+ }
+
+ setupTypingIndicator(chatInput);
+ setupDraftAutoSave(chatInput);
+ setupSendMessageDraftClear(chatInput);
+
+ checkScrollPosition();
+ }
+
+ window.sendMessage = sendMessage;
+ window.replyToMessage = replyToMessage;
+ window.cancelReply = cancelReply;
+ window.scrollToMessage = scrollToMessage;
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initChat);
+ } else {
+ initChat();
+ }
+})();
diff --git a/static/style.css b/static/style.css
index 3d1cf89..0e2a857 100644
--- a/static/style.css
+++ b/static/style.css
@@ -6,6 +6,11 @@ body {
color: #001858; /* blue */
}
+body.chat-page {
+ height: 100vh;
+ overflow: hidden;
+}
+
main {
display: flex;
flex-direction: column;
@@ -13,6 +18,12 @@ main {
padding: 25px;
}
+body.chat-page main {
+ padding: 0;
+ margin-top: 3em;
+ height: calc(100vh - 3em);
+}
+
main > header {
text-align: center;
margin-bottom: 1em;
@@ -343,6 +354,344 @@ p.thread-info {
background-color: #ffe0f0; /* Light pink background */
animation: highlight-fade 2s ease-out;
}
+
+/* Chat page styles */
+.chat-container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ border: none;
+ border-radius: 0;
+ background-color: #fef6e4;
+ box-shadow: none;
+}
+
+.chat-header {
+ padding: 10px;
+ text-align: center;
+ border-bottom: 1px solid #001858;
+ position: relative;
+}
+
+.connection-status {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.85em;
+ padding: 4px 10px;
+ border-radius: 12px;
+ background-color: rgba(0,0,0,0.1);
+}
+
+.connection-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ animation: pulse 2s ease-in-out infinite;
+}
+
+.connection-dot.connected {
+ background-color: #4ade80;
+}
+
+.connection-dot.connecting {
+ background-color: #fbbf24;
+}
+
+.connection-dot.disconnected {
+ background-color: #f87171;
+ animation: none;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.typing-indicator {
+ padding: 8px 12px;
+ background-color: #f3d2c1;
+ border-radius: 5px;
+ font-size: 0.85em;
+ font-style: italic;
+ color: #001858;
+ margin-bottom: 8px;
+ display: none;
+}
+
+.typing-indicator.visible {
+ display: block;
+}
+
+.typing-dots {
+ display: inline-block;
+ margin-left: 4px;
+}
+
+.typing-dots span {
+ animation: typing-blink 1.4s infinite;
+ display: inline-block;
+}
+
+.typing-dots span:nth-child(2) {
+ animation-delay: 0.2s;
+}
+
+.typing-dots span:nth-child(3) {
+ animation-delay: 0.4s;
+}
+
+@keyframes typing-blink {
+ 0%, 60%, 100% { opacity: 0.3; }
+ 30% { opacity: 1; }
+}
+
+.jump-to-bottom {
+ position: absolute;
+ bottom: 70px;
+ right: 20px;
+ background-color: #001858;
+ color: #fef6e4;
+ border: 2px solid #001858;
+ border-radius: 50%;
+ width: 45px;
+ height: 45px;
+ cursor: pointer;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.3em;
+ box-shadow: 0px 4px 12px rgba(0,0,0,0.3);
+ transition: transform 0.2s ease;
+ z-index: 100;
+}
+
+.jump-to-bottom.visible {
+ display: flex;
+}
+
+.jump-to-bottom:hover {
+ transform: translateY(2px);
+ background-color: #8bd3dd;
+}
+
+.jump-to-bottom .unread-badge {
+ position: absolute;
+ top: -5px;
+ right: -5px;
+ background-color: #f582ae;
+ color: #fef6e4;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.7em;
+ font-weight: bold;
+}
+
+.message-status {
+ font-size: 0.7em;
+ color: #666;
+ margin-left: 8px;
+}
+
+.message-status.sending {
+ color: #fbbf24;
+}
+
+.message-status.sent {
+ color: #4ade80;
+}
+
+.message-status.failed {
+ color: #f87171;
+ cursor: pointer;
+}
+
+.chat-breadcrumb {
+ background-color: #f3d2c1;
+ padding: 8px 12px;
+ border-bottom: 1px solid #001858;
+ font-size: 0.85em;
+ text-align: left;
+}
+
+.chat-breadcrumb a {
+ color: #001858;
+ text-decoration: none;
+}
+
+.chat-breadcrumb a:hover {
+ color: #f582ae;
+ text-decoration: underline;
+}
+
+.chat-breadcrumb-separator {
+ margin: 0 6px;
+ opacity: 0.6;
+}
+
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+}
+
+.chat-message {
+ margin-bottom: 8px;
+ max-width: 90%;
+ position: relative;
+}
+
+.chat-message-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 3px;
+}
+
+.chat-message-pfp {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ margin-right: 8px;
+}
+
+.chat-message-username {
+ font-weight: bold;
+ color: #001858;
+ font-size: 0.9em;
+}
+
+.chat-message-timestamp {
+ font-size: 0.7em;
+ color: #666;
+ margin-left: 8px;
+}
+
+.chat-message-content {
+ background-color: #f3d2c1;
+ padding: 6px 10px;
+ border-radius: 5px;
+ line-height: 1.3;
+ font-size: 0.9em;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ word-break: break-word;
+}
+
+.chat-message-reply {
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ border-radius: 5px;
+ margin-bottom: 3px;
+ font-size: 0.8em;
+ cursor: pointer;
+}
+
+.chat-message-mention {
+ color: #f582ae;
+ font-weight: bold;
+}
+
+.chat-input {
+ padding: 8px;
+ border-top: 1px solid #001858;
+ display: flex;
+ flex-direction: column;
+}
+
+.chat-input textarea {
+ resize: none;
+ height: 50px;
+ margin-bottom: 8px;
+ font-size: 0.9em;
+}
+
+.chat-input button {
+ align-self: flex-end;
+ width: auto;
+ padding: 6px 12px;
+ font-size: 0.9em;
+}
+
+.chat-message:hover .post-actions {
+ opacity: 1;
+}
+
+.post-actions a {
+ color: #001858;
+ text-decoration: none;
+ font-size: 0.8em;
+ padding: 2px 5px;
+ border: 1px solid #001858;
+ border-radius: 3px;
+}
+
+.post-actions a:hover {
+ background-color: #8bd3dd;
+ color: #fef6e4;
+}
+
+.autocomplete-popup {
+ position: absolute;
+ background-color: #fff;
+ border: 1px solid #001858;
+ border-radius: 5px;
+ max-height: 200px;
+ overflow-y: auto;
+ box-shadow: 0px 4px 8px rgba(0,0,0,0.2);
+ z-index: 1000;
+ display: none;
+}
+
+.autocomplete-item {
+ padding: 6px 10px;
+ cursor: pointer;
+ font-size: 0.9em;
+}
+
+.autocomplete-item:hover {
+ background-color: #f3d2c1;
+}
+
+.reply-indicator {
+ background-color: #001858;
+ color: #fef6e4;
+ padding: 5px 10px;
+ border-radius: 5px;
+ margin-bottom: 8px;
+ display: none;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.reply-indicator span {
+ font-size: 0.9em;
+}
+
+.reply-indicator button {
+ background: none;
+ border: none;
+ color: #fef6e4;
+ cursor: pointer;
+ font-size: 0.9em;
+ padding: 0 5px;
+ margin: 0;
+ width: auto;
+}
+
+.reply-indicator button:hover {
+ background: none;
+ color: #f582ae;
+}
/* Grouped messages (hide header for consecutive messages from same user) */
.chat-message.grouped {
margin-top: -4px;
@@ -440,6 +789,90 @@ p.thread-info {
border-color: #f582ae;
}
}
+
+ body.chat-page {
+ background-color: #333;
+ }
+
+ .chat-container {
+ background-color: #444;
+ }
+
+ .chat-breadcrumb {
+ background-color: #555;
+ }
+
+ .chat-breadcrumb a {
+ color: #fef6e4;
+ }
+
+ .typing-indicator {
+ background-color: #555;
+ color: #fef6e4;
+ }
+
+ .jump-to-bottom {
+ background-color: #fef6e4;
+ color: #001858;
+ border-color: #fef6e4;
+ }
+
+ .jump-to-bottom:hover {
+ background-color: #8bd3dd;
+ }
+
+ .chat-header {
+ border-color: #fef6e4;
+ }
+
+ .chat-message-username {
+ color: #fef6e4;
+ }
+
+ .chat-message-timestamp {
+ color: #aaa;
+ }
+
+ .chat-message-content {
+ background-color: #555;
+ }
+
+ .chat-input {
+ border-color: #fef6e4;
+ }
+
+ .autocomplete-popup {
+ background-color: #444;
+ border-color: #fef6e4;
+ color: #fef6e4;
+ }
+
+ .autocomplete-item:hover {
+ background-color: #555;
+ }
+
+ .post-actions a {
+ color: #fef6e4;
+ border-color: #fef6e4;
+ }
+
+ .post-actions a:hover {
+ background-color: #8bd3dd;
+ color: #001858;
+ }
+
+ .reply-indicator {
+ background-color: #222;
+ color: #fef6e4;
+ }
+
+ .reply-indicator button {
+ color: #fef6e4;
+ }
+
+ .reply-indicator button:hover {
+ color: #f582ae;
+ }
}
/* Breadcrumb navigation */
@@ -809,4 +1242,4 @@ input.error, textarea.error, select.error {
left: 10px;
max-width: none;
}
-}
\ No newline at end of file
+}
diff --git a/templates/base.html b/templates/base.html
index a41938a..10b8efa 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -5,13 +5,14 @@
{{.Title}}
+
-
+
{{template "navbar" .}}
- {{block "content" .}}{{end}}
+ {{template .ContentTemplate .}}
{{template "cookie_banner" .}}
-{{end}}
\ No newline at end of file
+{{end}}
diff --git a/templates/pages/chat.html b/templates/pages/chat.html
index 4d168db..d1f5e52 100644
--- a/templates/pages/chat.html
+++ b/templates/pages/chat.html
@@ -1,947 +1,60 @@
-{{define "chat"}}
-
-
-
- {{.Title}}
-
-
-
-
-
- {{template "navbar" .}}
-
-
-
-
-
-
- ...
-
- {{range .Messages}}
-
-
- {{if gt .ReplyTo 0}}
-
Replying to message...
- {{end}}
-
{{.Content}}
-
-
+{{define "chat"}}{{template "base" .}}{{end}}
+
+{{define "chat-content"}}
+
+
+
+
+
+ ...
+
+ {{range .Messages}}
+
+
-
-
-
-
- {{template "cookie_banner" .}}
-
-
-
-{{end}}
\ No newline at end of file
+ {{end}}
+
+
+
+
+
+
+{{end}}