Weiter am Chastity Game gearbeitet und Interaktionen zwischen Keyholder und Lockee hinzugefügt

This commit is contained in:
2026-03-16 23:16:45 +01:00
parent 57a7c78037
commit 97c6f0a131
399 changed files with 34194 additions and 2272 deletions

View File

@@ -324,6 +324,12 @@
let activePartnerId = null;
let pollTimer = null;
// Pagination state
let oldestSentAt = null; // ISO string of oldest visible message
let newestSentAt = null; // ISO string of newest visible message
let hasMoreOlder = false;
let isLoadingOlder = false;
// Load current user
fetch('/login/me')
.then(r => r.ok ? r.json() : null)
@@ -331,7 +337,6 @@
if (!user) return;
myId = user.userId;
loadConversations();
// Open thread from URL param
const urlPartnerId = new URLSearchParams(window.location.search).get('userId');
if (urlPartnerId) openThread(urlPartnerId);
})
@@ -380,13 +385,15 @@
async function openThread(partnerId, partnerName, partnerPic) {
activePartnerId = partnerId;
oldestSentAt = null;
newestSentAt = null;
hasMoreOlder = false;
isLoadingOlder = false;
// Update active state in list
document.querySelectorAll('.conv-item').forEach(li => {
li.classList.toggle('active', li.dataset.partnerId === partnerId);
});
// Update header if name not provided, fetch from conversation list or user lookup
if (!partnerName) {
const convItem = document.querySelector(`.conv-item[data-partner-id="${partnerId}"]`);
partnerName = convItem ? convItem.querySelector('.conv-name').textContent : '…';
@@ -405,52 +412,107 @@
document.getElementById('threadInputWrap').style.display = '';
document.getElementById('msgInput').focus();
// Mobile: hide list, show thread
if (window.innerWidth <= 768) showThread();
await loadThread();
// Clear and load latest messages
const container = document.getElementById('threadMessages');
container.innerHTML = '';
await loadInitialThread();
startPolling();
}
async function loadThread() {
async function loadInitialThread() {
if (!activePartnerId) return;
try {
const res = await fetch('/social/messages/' + activePartnerId);
if (!res.ok) return;
const msgs = await res.json();
renderThread(msgs);
loadConversations(); // refresh unread counts in list
const { messages, hasMore } = await res.json();
const container = document.getElementById('threadMessages');
container.innerHTML = '';
if (messages.length === 0) {
container.appendChild(Object.assign(document.createElement('div'), {
className: 'thread-placeholder',
textContent: 'Noch keine Nachrichten. Schreib als Erster!'
}));
oldestSentAt = null;
newestSentAt = null;
hasMoreOlder = false;
} else {
// messages are oldest-first from backend
messages.forEach(m => container.appendChild(buildBubble(m)));
oldestSentAt = messages[0].sentAt;
newestSentAt = messages[messages.length - 1].sentAt;
hasMoreOlder = hasMore;
}
container.scrollTop = container.scrollHeight;
loadConversations();
} catch (e) { console.error(e); }
}
function renderThread(msgs) {
const container = document.getElementById('threadMessages');
container.innerHTML = '';
if (msgs.length === 0) {
container.appendChild(Object.assign(document.createElement('div'), {
className: 'thread-placeholder',
textContent: 'Noch keine Nachrichten. Schreib als Erster!'
}));
return;
}
// msgs are newest-first; reverse to show oldest first
[...msgs].reverse().forEach(m => {
const isMe = m.senderId === myId;
const time = new Date(m.sentAt).toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit' });
const wrap = document.createElement('div');
wrap.className = 'bubble-wrap ' + (isMe ? 'me' : 'them');
const isImg = m.text.startsWith('data:image/');
const content = isImg
? `<img src="${m.text}" class="bubble-img" onclick="openLightbox(this.src)" alt="Bild">`
: esc(m.text);
wrap.innerHTML = `
<div class="bubble">${content}</div>
<div class="bubble-time">${time}</div>`;
container.appendChild(wrap);
});
container.scrollTop = container.scrollHeight;
async function loadOlderMessages() {
if (!activePartnerId || !hasMoreOlder || isLoadingOlder || !oldestSentAt) return;
isLoadingOlder = true;
try {
const res = await fetch('/social/messages/' + activePartnerId + '?before=' + encodeURIComponent(oldestSentAt));
if (!res.ok) return;
const { messages, hasMore } = await res.json();
if (messages.length === 0) { hasMoreOlder = false; return; }
const container = document.getElementById('threadMessages');
const prevHeight = container.scrollHeight;
// Prepend older messages (oldest-first order)
const frag = document.createDocumentFragment();
messages.forEach(m => frag.appendChild(buildBubble(m)));
container.prepend(frag);
// Restore scroll position
container.scrollTop = container.scrollHeight - prevHeight;
oldestSentAt = messages[0].sentAt;
hasMoreOlder = hasMore;
} catch (e) { console.error(e); }
finally { isLoadingOlder = false; }
}
async function pollNewMessages() {
if (!activePartnerId || !newestSentAt) return;
try {
const res = await fetch('/social/messages/' + activePartnerId + '?after=' + encodeURIComponent(newestSentAt));
if (!res.ok) return;
const { messages } = await res.json();
if (messages.length === 0) return;
const container = document.getElementById('threadMessages');
const atBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 60;
// Remove placeholder if present
const ph = container.querySelector('.thread-placeholder');
if (ph) ph.remove();
messages.forEach(m => container.appendChild(buildBubble(m)));
newestSentAt = messages[messages.length - 1].sentAt;
if (atBottom) container.scrollTop = container.scrollHeight;
loadConversations();
} catch (e) { console.error(e); }
}
function buildBubble(m) {
const isMe = m.senderId === myId;
const time = new Date(m.sentAt).toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit' });
const wrap = document.createElement('div');
wrap.className = 'bubble-wrap ' + (isMe ? 'me' : 'them');
wrap.dataset.sentAt = m.sentAt;
const isImg = m.text.startsWith('data:image/');
const content = isImg
? `<img src="${m.text}" class="bubble-img" onclick="openLightbox(this.src)" alt="Bild">`
: linkify(m.text);
wrap.innerHTML = `
<div class="bubble">${content}</div>
<div class="bubble-time">${time}</div>`;
return wrap;
}
// Scroll-up → load older messages
document.getElementById('threadMessages').addEventListener('scroll', function () {
if (this.scrollTop < 80 && hasMoreOlder && !isLoadingOlder) {
loadOlderMessages();
}
});
async function sendMsg() {
if (!activePartnerId) return;
const input = document.getElementById('msgInput');
@@ -463,7 +525,7 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receiverId: activePartnerId, text })
});
await loadThread();
await pollNewMessages();
} catch (e) { console.error(e); }
}
@@ -473,10 +535,9 @@
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(loadThread, 10000);
pollTimer = setInterval(pollNewMessages, 10000);
}
// Mobile: show/hide panes
function showThread() {
document.getElementById('convListPane').classList.add('hidden');
document.getElementById('threadPane').classList.remove('hidden');
@@ -488,7 +549,6 @@
activePartnerId = null;
}
// ── Lightbox ──
function openLightbox(src) {
document.getElementById('lightboxImg').src = src;
document.getElementById('lightbox').classList.add('open');
@@ -506,6 +566,16 @@
return d.innerHTML;
}
function linkify(text) {
// HTML-escapen, Zeilenumbrüche als <br>, URLs als klickbare Links
const escaped = esc(text)
.replace(/\n/g, '<br>');
return escaped.replace(
/(https?:\/\/[^\s<>"]+)/g,
url => `<a href="${url}" target="_blank" rel="noopener noreferrer" style="color:inherit;text-decoration:underline;word-break:break-all;">${url}</a>`
);
}
// ── Emoji-Picker ──
const EMOJIS = [
'😀','😂','🤣','😅','😊','😍','🥰','😘','😎','🤩',
@@ -551,7 +621,6 @@
document.getElementById('imgFile').addEventListener('change', async function () {
const file = this.files[0];
if (!file || !activePartnerId) return;
// Frisches Input-Element einsetzen, damit jedes weitere Bild (auch dasselbe) wählbar bleibt
const fresh = this.cloneNode(false);
this.replaceWith(fresh);
attachImgHandler();
@@ -562,7 +631,7 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receiverId: activePartnerId, text: dataUrl })
});
await loadThread();
await pollNewMessages();
} catch (e) { console.error(e); }
});
})();