541 lines
19 KiB
HTML
541 lines
19 KiB
HTML
{{define "chat"}}
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>{{.Title}}</title>
|
||
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||
<script src="{{.StaticPath}}/app.js" defer></script>
|
||
<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: 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;
|
||
}
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
/* New style for highlighted messages */
|
||
.chat-message-highlighted {
|
||
border: 2px solid #f582ae; /* Pink border */
|
||
background-color: #ffe0f0; /* Light pink background */
|
||
animation: highlight-fade 2s ease-out;
|
||
}
|
||
@keyframes highlight-fade {
|
||
from {
|
||
background-color: #f582ae;
|
||
border-color: #f582ae;
|
||
}
|
||
to {
|
||
background-color: #ffe0f0;
|
||
border-color: #f582ae;
|
||
}
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
.chat-container {
|
||
background-color: #444;
|
||
}
|
||
.chat-breadcrumb {
|
||
background-color: #555;
|
||
}
|
||
.chat-breadcrumb a {
|
||
color: #fef6e4;
|
||
}
|
||
.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;
|
||
}
|
||
/* Dark mode highlight */
|
||
.chat-message-highlighted {
|
||
border: 2px solid #f582ae; /* Pink border */
|
||
background-color: #6a0e3f; /* Darker pink background */
|
||
animation: highlight-fade-dark 2s ease-out;
|
||
}
|
||
@keyframes highlight-fade-dark {
|
||
from {
|
||
background-color: #f582ae;
|
||
border-color: #f582ae;
|
||
}
|
||
to {
|
||
background-color: #6a0e3f;
|
||
border-color: #f582ae;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
{{template "navbar" .}}
|
||
<main>
|
||
<div class="chat-container">
|
||
<div class="chat-breadcrumb">
|
||
<a href="{{.BasePath}}/">Home</a>
|
||
<span class="chat-breadcrumb-separator">›</span>
|
||
<a href="{{.BasePath}}/boards/">Boards</a>
|
||
<span class="chat-breadcrumb-separator">›</span>
|
||
<span>{{.Board.Name}}</span>
|
||
</div>
|
||
<header class="chat-header">
|
||
<h2>{{.Board.Name}}</h2>
|
||
<p>{{.Board.Description}}</p>
|
||
</header>
|
||
<div class="chat-messages" id="chat-messages">
|
||
{{range .Messages}}
|
||
<div class="chat-message{{if .Mentions}}{{range .Mentions}}{{if eq . $.CurrentUsername}} chat-message-highlighted{{end}}{{end}}{{end}}" 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 message...</div>
|
||
{{end}}
|
||
<div class="chat-message-content">{{.Content}}</div>
|
||
<div class="post-actions">
|
||
<a href="javascript:void(0)" onclick="replyToMessage({{printf "%v" .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}};
|
||
const currentUsername = "{{.CurrentUsername}}";
|
||
|
||
function connectWebSocket() {
|
||
const boardID = {{.Board.ID}};
|
||
ws = new WebSocket('ws://' + window.location.host + '{{.BasePath}}/chat/?ws=true&id=' + boardID);
|
||
ws.onmessage = function(event) {
|
||
const msg = JSON.parse(event.data);
|
||
appendMessage(msg);
|
||
};
|
||
ws.onclose = function() {
|
||
console.log("WebSocket closed, reconnecting...");
|
||
setTimeout(connectWebSocket, 5000);
|
||
};
|
||
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');
|
||
let highlightClass = '';
|
||
if (msg.mentions && msg.mentions.includes(currentUsername)) {
|
||
highlightClass = ' chat-message-highlighted';
|
||
}
|
||
msgDiv.className = 'chat-message' + highlightClass;
|
||
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 to message...</div>` : '';
|
||
|
||
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">${msg.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.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;
|
||
}
|
||
|
||
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();
|
||
document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
{{end}} |