469 lines
17 KiB
HTML
469 lines
17 KiB
HTML
{{define "chat"}}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>{{.Title}}</title>
|
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
main {
|
|
padding: 0;
|
|
margin-top: 3em; /* Space for navbar */
|
|
height: calc(100vh - 3em);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
.chat-container {
|
|
width: 100%;
|
|
height: calc(100% - 2em); /* Adjust for header */
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: none;
|
|
border-radius: 0;
|
|
background-color: #fef6e4;
|
|
box-shadow: none;
|
|
}
|
|
.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;
|
|
}
|
|
.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;
|
|
}
|
|
.post-actions {
|
|
position: absolute;
|
|
top: 5px;
|
|
right: 5px;
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
.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;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
.chat-container {
|
|
background-color: #444;
|
|
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;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{{template "navbar" .}}
|
|
<main>
|
|
<header style="display: none;">
|
|
<h2>General Chat</h2>
|
|
</header>
|
|
<div class="chat-container">
|
|
<div class="chat-messages" id="chat-messages">
|
|
{{range .Messages}}
|
|
<div class="chat-message" id="msg-{{.ID}}">
|
|
<div class="chat-message-header">
|
|
{{if .PfpFileID.Valid}}
|
|
<img src="{{$.BasePath}}/file?id={{.PfpFileID.Int64}}" alt="PFP" class="chat-message-pfp">
|
|
{{else}}
|
|
<div class="chat-message-pfp" style="background-color: #001858;"></div>
|
|
{{end}}
|
|
<span class="chat-message-username">{{.Username}}</span>
|
|
<span class="chat-message-timestamp">{{.Timestamp.Format "02/01/2006 15:04"}}</span>
|
|
</div>
|
|
{{if gt .ReplyTo 0}}
|
|
<div class="chat-message-reply" onclick="scrollToMessage({{.ReplyTo}})">Replying to {{.Username}}</div>
|
|
{{end}}
|
|
<div class="chat-message-content">{{.Content | html}}</div>
|
|
<div class="post-actions">
|
|
<a href="javascript:void(0)" onclick="replyToMessage({{.ID}}, '{{.Username}}')">Reply</a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
<div class="chat-input">
|
|
<div id="reply-indicator" class="reply-indicator">
|
|
<span id="reply-username">Replying to </span>
|
|
<button onclick="cancelReply()">X</button>
|
|
</div>
|
|
<textarea id="chat-input-text" placeholder="Type a message..."></textarea>
|
|
<button onclick="sendMessage()">Send</button>
|
|
</div>
|
|
</div>
|
|
<div id="autocomplete-popup" class="autocomplete-popup"></div>
|
|
</main>
|
|
{{template "cookie_banner" .}}
|
|
<script>
|
|
let ws;
|
|
let autocompleteActive = false;
|
|
let replyToId = -1;
|
|
const allUsernames = {{.AllUsernames}};
|
|
|
|
function connectWebSocket() {
|
|
ws = new WebSocket('ws://' + window.location.host + '{{.BasePath}}/chat/?ws=true');
|
|
ws.onmessage = function(event) {
|
|
const msg = JSON.parse(event.data);
|
|
appendMessage(msg);
|
|
};
|
|
ws.onclose = function() {
|
|
console.log("WebSocket closed, reconnecting...");
|
|
setTimeout(connectWebSocket, 5000); // Reconnect after 5s
|
|
};
|
|
ws.onerror = function(error) {
|
|
console.error("WebSocket error:", error);
|
|
};
|
|
}
|
|
|
|
function sendMessage() {
|
|
const input = document.getElementById('chat-input-text');
|
|
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');
|
|
}
|
|
}
|
|
|
|
function appendMessage(msg) {
|
|
const messages = document.getElementById('chat-messages');
|
|
const msgDiv = document.createElement('div');
|
|
msgDiv.className = 'chat-message';
|
|
msgDiv.id = 'msg-' + msg.id;
|
|
let pfpHTML = `<div class="chat-message-pfp" style="background-color: #001858;"></div>`;
|
|
if (msg.pfpFileId && msg.pfpFileId.Valid) {
|
|
pfpHTML = `<img src="{{.BasePath}}/file?id=${msg.pfpFileId.Int64}&t=${new Date().getTime()}" alt="PFP" class="chat-message-pfp">`;
|
|
}
|
|
let replyHTML = msg.replyTo > 0 ? `<div class="chat-message-reply" onclick="scrollToMessage(${msg.replyTo})">Replying...</div>` : '';
|
|
let content = msg.content.replace(/@(\w+)/g, '<span class="chat-message-mention">@$1</span>');
|
|
|
|
msgDiv.innerHTML = `
|
|
<div class="chat-message-header">
|
|
${pfpHTML}
|
|
<span class="chat-message-username">${msg.username}</span>
|
|
<span class="chat-message-timestamp">${new Date(msg.timestamp).toLocaleString()}</span>
|
|
</div>
|
|
${replyHTML}
|
|
<div class="chat-message-content">${content}</div>
|
|
<div class="post-actions">
|
|
<a href="javascript:void(0)" onclick="replyToMessage(${msg.id}, '${msg.username}')">Reply</a>
|
|
</div>
|
|
`;
|
|
messages.appendChild(msgDiv);
|
|
messages.scrollTop = messages.scrollHeight;
|
|
}
|
|
|
|
function replyToMessage(id, username) {
|
|
replyToId = id;
|
|
const replyIndicator = document.getElementById('reply-indicator');
|
|
const replyUsernameSpan = document.getElementById('reply-username');
|
|
replyUsernameSpan.textContent = `Replying to ${username}`;
|
|
replyIndicator.style.display = 'flex';
|
|
document.getElementById('chat-input-text').focus();
|
|
}
|
|
|
|
function cancelReply() {
|
|
replyToId = -1;
|
|
const replyIndicator = document.getElementById('reply-indicator');
|
|
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');
|
|
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');
|
|
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';
|
|
let div = document.getElementById(mirrorDivId);
|
|
if (!div) {
|
|
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);
|
|
|
|
return { top: span.offsetTop, left: span.offsetLeft };
|
|
}
|
|
|
|
document.getElementById('chat-input-text').addEventListener('input', (e) => {
|
|
const input = e.target;
|
|
const text = input.value;
|
|
const caretPos = input.selectionStart;
|
|
const popup = document.getElementById('autocomplete-popup');
|
|
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;
|
|
});
|
|
|
|
document.getElementById('chat-input-text').addEventListener('blur', () => {
|
|
setTimeout(() => {
|
|
if (!document.querySelector('.autocomplete-popup:hover')) {
|
|
document.getElementById('autocomplete-popup').style.display = 'none';
|
|
autocompleteActive = false;
|
|
}
|
|
}, 150);
|
|
});
|
|
|
|
document.getElementById('chat-input-text').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
window.onload = function() {
|
|
connectWebSocket();
|
|
|
|
// Highlight mentions in pre-loaded messages
|
|
document.querySelectorAll('.chat-message-content').forEach(function(el) {
|
|
const text = el.innerHTML; // The Go template already escaped it for security
|
|
const newHTML = text.replace(/@(\w+)/g, '<span class="chat-message-mention">@$1</span>');
|
|
el.innerHTML = newHTML;
|
|
});
|
|
|
|
document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}} |