Weiter am Chastity Game gearbeitet und Interaktionen zwischen Keyholder und Lockee hinzugefügt
This commit is contained in:
@@ -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); }
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user