Amnezichat/client/static/index.html

313 lines
11 KiB
HTML
Raw Normal View History

2025-02-08 22:29:25 +03:00
<!DOCTYPE html>
<html lang="en">
<head>
2025-04-14 14:57:01 +03:00
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Amnezichat</title>
<link rel="stylesheet" href="/static/styles.css" />
<script src="/static/purify.min.js"></script>
</head>
<body onload="fetchMessages(); requestNotificationPermission(); setInterval(fetchMessages, 3000);">
<div class="container">
<div id="messages"></div>
<div class="input-container">
<input type="text" id="messageInput" placeholder="Type a message..." autocomplete="off" />
<button onclick="sendMessage()">Send</button>
2025-04-19 13:30:28 +03:00
<button onclick="document.getElementById('mediaInput').click()">📷</button>
<button onclick="startVoiceRecording()" id="micButton">🎤</button>
2025-04-14 14:57:01 +03:00
<button onclick="toggleSettings()">Settings</button>
</div>
</div>
2025-02-21 22:42:28 +03:00
2025-04-19 13:30:28 +03:00
<div id="recordingStatus" style="display:none; align-items:center; gap:10px; margin-top: 10px;">
<span id="timer">00:00</span>
<button onclick="cancelVoiceRecording()" style="background-color:red; color:white;">Cancel ❌</button>
</div>
2025-04-14 14:57:01 +03:00
<div id="settings">
<div class="settings-content">
<h2>Settings</h2>
<input type="file" accept="image/*" id="profilePicInput" style="display:none;" onchange="handleProfilePicChange(event)" />
<button onclick="document.getElementById('profilePicInput').click()">Choose Profile Picture</button>
<button onclick="toggleTheme()">Toggle Light/Dark Mode</button>
<button onclick="closeSettings()">Close</button>
</div>
2025-04-19 13:30:28 +03:00
</div>
2025-02-08 22:29:25 +03:00
2025-04-14 14:57:01 +03:00
<div id="mediaModal" onclick="closeMediaModal()">
<span class="close-btn" onclick="closeMediaModal(); event.stopPropagation();">&times;</span>
</div>
2025-02-08 22:29:25 +03:00
2025-04-19 13:30:28 +03:00
<input type="file" accept="image/*,video/*" id="mediaInput" style="display:none;" onchange="handleMediaChange(event)" />
2025-04-14 14:57:01 +03:00
<script>
let profilePicBase64 = localStorage.getItem("profilePic") || "";
2025-04-19 13:30:28 +03:00
let notifiedMessageSet = new Set(JSON.parse(localStorage.getItem("notifiedMessages") || "[]"));
let mediaRecorder;
let audioChunks = [];
let recordingStartTime;
let recordingTimerInterval;
function hashMessage(message) {
return message.replace(/<[^>]*>/g, '').trim();
}
2025-02-08 22:29:25 +03:00
2025-04-14 14:57:01 +03:00
function showNotification(message) {
2025-04-19 13:30:28 +03:00
const messageHash = hashMessage(message);
if (!notifiedMessageSet.has(messageHash) && Notification.permission === "granted") {
2025-04-14 14:57:01 +03:00
new Notification("New Message", {
2025-04-19 13:30:28 +03:00
body: messageHash
2025-04-14 14:57:01 +03:00
});
2025-04-19 13:30:28 +03:00
notifiedMessageSet.add(messageHash);
localStorage.setItem("notifiedMessages", JSON.stringify(Array.from(notifiedMessageSet)));
2025-04-14 14:57:01 +03:00
}
}
2025-02-08 22:29:25 +03:00
2025-04-14 14:57:01 +03:00
function requestNotificationPermission() {
if (Notification.permission !== "granted") {
Notification.requestPermission();
}
}
2025-02-08 22:29:25 +03:00
2025-04-14 14:57:01 +03:00
function handleProfilePicChange(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
profilePicBase64 = reader.result;
localStorage.setItem("profilePic", profilePicBase64);
};
reader.readAsDataURL(file);
}
}
2025-02-08 22:29:25 +03:00
2025-04-14 14:57:01 +03:00
function handleMediaChange(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = async () => {
2025-04-19 13:30:28 +03:00
const mediaBase64 = DOMPurify.sanitize(reader.result);
2025-04-14 14:57:01 +03:00
let message = `<pfp>${profilePicBase64}</pfp><media>${mediaBase64}</media>`;
await fetch('/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
fetchMessages();
};
reader.readAsDataURL(file);
}
}
async function fetchMessages() {
const res = await fetch('/messages');
const messages = await res.json();
const messagesDiv = document.getElementById('messages');
const base64Img = /^data:image\/(png|jpeg|jpg|gif|svg\+xml);base64,/;
const base64Vid = /^data:video\/(mp4|webm|ogg);base64,/;
2025-04-19 13:30:28 +03:00
const base64Audio = /^data:audio\/(webm|ogg);base64,/;
2025-04-14 14:57:01 +03:00
2025-04-19 13:30:28 +03:00
let messagesHTML = messages.map(msg => {
2025-04-14 14:57:01 +03:00
const pfpMatch = msg.match(/<pfp>(.*?)<\/pfp>/);
const mediaMatch = msg.match(/<media>(.*?)<\/media>/);
2025-04-19 13:30:28 +03:00
const audioMatch = msg.match(/<audio>(.*?)<\/audio>/);
const messageTextRaw = msg.replace(/<pfp>.*?<\/pfp>/, '').replace(/<media>.*?<\/media>/, '').replace(/<audio>.*?<\/audio>/, '').trim();
const messageText = DOMPurify.sanitize(messageTextRaw);
2025-04-14 14:57:01 +03:00
const profilePicSrc = pfpMatch && base64Img.test(pfpMatch[1])
2025-04-19 13:30:28 +03:00
? DOMPurify.sanitize(pfpMatch[1])
2025-04-14 14:57:01 +03:00
: '/static/default_pfp.jpg';
const profilePic = `<img src="${profilePicSrc}" class="profile-pic" alt="Profile Picture">`;
let media = "";
if (mediaMatch) {
2025-04-19 13:30:28 +03:00
const src = DOMPurify.sanitize(mediaMatch[1]);
2025-04-14 14:57:01 +03:00
if (base64Img.test(src)) {
media = `<img src="${src}" class="media-img" alt="Media" onclick="openMediaModal('${src}', false)">`;
} else if (base64Vid.test(src)) {
2025-04-19 13:30:28 +03:00
media = `<video class="media-video" controls onclick="openMediaModal('${src}', true)">
<source src="${src}" type="video/mp4">Your browser does not support video.
</video>`;
2025-04-14 14:57:01 +03:00
}
2025-02-08 22:29:25 +03:00
}
2025-04-19 13:30:28 +03:00
if (audioMatch) {
const src = DOMPurify.sanitize(audioMatch[1]);
media = `<audio controls class="media-audio">
<source src="${src}" type="audio/webm">Your browser does not support the audio element.
</audio>`;
}
2025-04-14 14:57:01 +03:00
showNotification(messageText || 'New media message');
return `
<div class="message-row">
${profilePic}
<div class="message-bubble">
2025-04-19 13:30:28 +03:00
${messageText ? `<p>${messageText}</p>` : ''}
2025-04-14 14:57:01 +03:00
${media}
</div>
</div>`;
}).join('');
2025-04-19 13:30:28 +03:00
messagesDiv.innerHTML = DOMPurify.sanitize(messagesHTML, {
SAFE_FOR_JQUERY: true,
ADD_ATTR: ['onclick']
});
2025-04-14 14:57:01 +03:00
messagesDiv.scrollTo({ top: messagesDiv.scrollHeight, behavior: 'smooth' });
}
async function sendMessage() {
const input = document.getElementById('messageInput');
const msg = input.value.trim();
if (!msg) return;
2025-04-19 13:30:28 +03:00
const sanitizedMsg = DOMPurify.sanitize(msg, {
ALLOWED_TAGS: [],
ALLOWED_ATTR: []
});
2025-04-14 14:57:01 +03:00
const message = `<pfp>${profilePicBase64}</pfp>${sanitizedMsg}`;
await fetch('/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
input.value = '';
fetchMessages();
}
function toggleSettings() {
const modal = document.getElementById('settings');
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
}
function closeSettings() {
document.getElementById('settings').style.display = 'none';
}
function toggleTheme() {
document.body.classList.toggle('light-theme');
}
function openMediaModal(src, isVideo = false) {
const modal = document.getElementById("mediaModal");
modal.innerHTML = `<span class="close-btn" onclick="closeMediaModal(); event.stopPropagation();">&times;</span>`;
if (isVideo) {
const video = document.createElement('video');
video.controls = true;
video.src = src;
video.autoplay = true;
video.style.maxWidth = '90%';
video.style.maxHeight = '90%';
video.style.borderRadius = '10px';
modal.appendChild(video);
} else {
const img = document.createElement('img');
img.src = src;
img.style.maxWidth = '90%';
img.style.maxHeight = '90%';
img.style.borderRadius = '10px';
modal.appendChild(img);
}
modal.style.display = "flex";
}
function closeMediaModal() {
const modal = document.getElementById("mediaModal");
modal.style.display = "none";
modal.innerHTML = `<span class="close-btn" onclick="closeMediaModal(); event.stopPropagation();">&times;</span>`;
}
document.addEventListener("DOMContentLoaded", () => {
const savedPic = localStorage.getItem("profilePic");
if (savedPic) {
profilePicBase64 = savedPic;
}
});
2025-04-19 13:30:28 +03:00
function startVoiceRecording() {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
recordingStartTime = Date.now();
mediaRecorder.ondataavailable = event => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = async () => {
clearInterval(recordingTimerInterval);
document.getElementById("recordingStatus").style.display = "none";
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
if (!audioBlob || !audioChunks.length) return;
const reader = new FileReader();
reader.onloadend = async () => {
const audioBase64 = DOMPurify.sanitize(reader.result);
const message = `<pfp>${profilePicBase64}</pfp><audio>${audioBase64}</audio>`;
await fetch('/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
fetchMessages();
};
reader.readAsDataURL(audioBlob);
};
mediaRecorder.start();
startTimer();
document.getElementById('micButton').innerText = '⏹️';
document.getElementById('micButton').onclick = stopVoiceRecording;
document.getElementById("recordingStatus").style.display = "flex";
})
.catch(err => {
alert('Microphone access denied.');
console.error(err);
});
}
function stopVoiceRecording() {
mediaRecorder.stop();
document.getElementById('micButton').innerText = '🎤';
document.getElementById('micButton').onclick = startVoiceRecording;
}
function cancelVoiceRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
audioChunks = [];
clearInterval(recordingTimerInterval);
document.getElementById("recordingStatus").style.display = "none";
document.getElementById('micButton').innerText = '🎤';
document.getElementById('micButton').onclick = startVoiceRecording;
}
function startTimer() {
const timerElement = document.getElementById('timer');
recordingTimerInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
const minutes = String(Math.floor(elapsed / 60)).padStart(2, '0');
const seconds = String(elapsed % 60).padStart(2, '0');
timerElement.textContent = `${minutes}:${seconds}`;
if (elapsed >= 60) {
cancelVoiceRecording();
}
}, 1000);
}
2025-04-14 14:57:01 +03:00
</script>
2025-02-08 22:29:25 +03:00
</body>
</html>