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();">×</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();">×</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();">×</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>
|