Files
xxx-sphere-web/bin/main/static/community/nachrichten.html
Mario e2a71ab096
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Hashtags eingeführt
2026-04-11 01:14:33 +02:00

743 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nachrichten xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* Override .main and .content for full-height chat layout */
.main { overflow: hidden; }
.msg-layout {
display: flex;
height: calc(100vh - 3rem);
overflow: hidden;
}
/* Left: conversation list */
.conv-list-pane {
width: 260px;
flex-shrink: 0;
border-right: 1px solid var(--color-secondary);
display: flex;
flex-direction: column;
overflow: hidden;
}
.conv-list-header {
padding: 1.25rem 1rem 0.75rem;
font-weight: 700;
font-size: 1rem;
border-bottom: 1px solid var(--color-secondary);
flex-shrink: 0;
}
.conv-list {
flex: 1;
overflow-y: auto;
list-style: none;
margin: 0;
padding: 0.25rem 0;
}
.conv-item {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.7rem 1rem;
cursor: pointer;
border-left: 3px solid transparent;
transition: background 0.15s, border-color 0.15s;
}
.conv-item:hover { background: var(--color-secondary); }
.conv-item.active { background: var(--color-secondary); border-left-color: var(--color-primary); }
.conv-avatar {
width: 38px; height: 38px;
border-radius: 50%;
background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1rem;
flex-shrink: 0;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.1);
}
.conv-avatar img { width: 100%; height: 100%; object-fit: cover; }
.conv-info { flex: 1; min-width: 0; }
.conv-name { font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.conv-preview { font-size: 0.78rem; color: var(--color-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.conv-unread {
background: var(--color-primary);
color: #fff;
font-size: 0.65rem;
font-weight: 700;
border-radius: 9999px;
padding: 0.1rem 0.35rem;
flex-shrink: 0;
}
/* Right: thread */
.thread-pane {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.thread-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-secondary);
font-weight: 700;
font-size: 1rem;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.thread-back {
display: none;
background: none;
border: none;
color: var(--color-muted);
cursor: pointer;
font-size: 1.1rem;
padding: 0;
margin: 0;
}
.thread-messages {
flex: 1;
overflow-y: auto;
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.thread-placeholder {
color: var(--color-muted);
text-align: center;
margin-top: 3rem;
font-size: 0.95rem;
}
.bubble-wrap {
display: flex;
flex-direction: column;
}
.bubble-wrap.me { align-items: flex-end; }
.bubble-wrap.them { align-items: flex-start; }
.bubble {
max-width: 70%;
padding: 0.5rem 0.9rem;
border-radius: 14px;
font-size: 0.9rem;
line-height: 1.45;
word-break: break-word;
}
.bubble-wrap.me .bubble {
background: var(--color-primary);
color: #fff;
border-bottom-right-radius: 4px;
}
.bubble-wrap.them .bubble {
background: var(--color-secondary);
color: var(--color-text);
border-bottom-left-radius: 4px;
}
.bubble-time {
font-size: 0.7rem;
color: var(--color-muted);
margin-top: 0.15rem;
padding: 0 0.25rem;
}
.thread-input-wrap {
flex-shrink: 0;
position: relative;
}
.thread-input-area {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.65rem 1rem;
border-top: 1px solid var(--color-secondary);
}
.thread-input-area input {
flex: 1;
}
.btn-icon {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
padding: 0.3rem 0.4rem;
width: auto;
margin-top: 0;
flex-shrink: 0;
opacity: 0.65;
border-radius: 6px;
transition: opacity 0.15s, background 0.15s;
line-height: 1;
}
.btn-icon:hover { opacity: 1; background: var(--color-secondary); }
.btn-send {
width: auto;
margin-top: 0;
padding: 0.55rem 1rem;
flex-shrink: 0;
}
.emoji-picker {
position: absolute;
bottom: 100%;
left: 0; right: 0;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px 10px 0 0;
padding: 0.5rem;
display: none;
flex-wrap: wrap;
gap: 2px;
max-height: 160px;
overflow-y: auto;
box-shadow: 0 -4px 16px rgba(0,0,0,0.4);
z-index: 50;
}
.emoji-picker.open { display: flex; }
.emoji-picker button {
background: none;
border: none;
font-size: 1.3rem;
cursor: pointer;
padding: 0.2rem 0.3rem;
border-radius: 4px;
width: auto;
margin-top: 0;
line-height: 1;
}
.emoji-picker button:hover { background: var(--color-secondary); }
.bubble-img {
max-width: 220px;
max-height: 220px;
border-radius: 8px;
display: block;
cursor: zoom-in;
object-fit: contain;
}
.lightbox {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.85);
z-index: 500;
align-items: center;
justify-content: center;
}
.lightbox.open { display: flex; }
.lightbox img {
max-width: 90vw;
max-height: 90vh;
border-radius: 6px;
object-fit: contain;
box-shadow: 0 8px 40px rgba(0,0,0,0.7);
}
.lightbox-close {
position: fixed !important;
top: 1rem !important;
right: 1rem !important;
background: rgba(0,0,0,0.55) !important;
border: 1px solid rgba(255,255,255,0.2) !important;
color: #fff !important;
font-size: 1.1rem !important;
width: 2.2rem !important;
height: 2.2rem !important;
border-radius: 50% !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
margin: 0 !important;
line-height: 1 !important;
transition: background 0.15s !important;
z-index: 501;
}
.lightbox-close:hover { background: rgba(0,0,0,0.85) !important; }
/* Mobile */
@media (max-width: 768px) {
.msg-layout { height: auto; flex-direction: column; }
.conv-list-pane { width: 100%; border-right: none; border-bottom: 1px solid var(--color-secondary); max-height: 240px; }
.conv-list-pane.hidden { display: none; }
.thread-pane.hidden { display: none; }
.thread-back { display: block; }
}
</style>
</head>
<body class="app">
<div class="main">
<div class="msg-layout">
<!-- Left pane: conversation list -->
<div class="conv-list-pane" id="convListPane">
<div class="conv-list-header">Nachrichten</div>
<ul class="conv-list" id="convList">
<li style="padding:1rem; color:var(--color-muted); font-size:0.9rem;">Wird geladen…</li>
</ul>
</div>
<!-- Right pane: thread -->
<div class="thread-pane" id="threadPane">
<div class="thread-header" id="threadHeader">
<button class="thread-back" id="backBtn" onclick="showList()" aria-label="Zurück"></button>
<div class="conv-avatar" id="threadPartnerAvatar" style="display:none;"></div>
<span id="threadPartnerName">Konversation auswählen</span>
</div>
<div class="thread-messages" id="threadMessages">
<div class="thread-placeholder" id="threadPlaceholder">Wähle eine Konversation aus oder schreibe jemanden direkt an.</div>
</div>
<div class="thread-input-wrap" id="threadInputWrap" style="display:none;">
<div class="emoji-picker" id="emojiPicker"></div>
<div class="thread-input-area">
<button class="btn-icon" id="emojiBtn" onclick="toggleEmoji()" title="Emoji">😊</button>
<input type="text" id="msgInput" placeholder="Nachricht eingeben…" autocomplete="off">
<input type="file" id="imgFile" accept="image/*" style="display:none;">
<button class="btn-icon" onclick="document.getElementById('imgFile').click()" title="Bild senden">📷</button>
<button class="btn-send" onclick="sendMsg()"></button>
</div>
</div>
</div>
</div>
</div>
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
<img id="lightboxImg" src="" alt="" onclick="event.stopPropagation()">
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Schließen"></button>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
const SUPPORT_USER_ID = 'dbf1e35a-e331-3211-9889-d0d21f386028';
let myId = null;
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)
.then(user => {
if (!user) return;
myId = user.userId;
loadConversations();
const params = new URLSearchParams(window.location.search);
const urlPartnerId = params.get('partnerId') || params.get('userId');
if (urlPartnerId) openThread(urlPartnerId);
})
.catch(() => {});
async function loadConversations() {
try {
const res = await fetch('/social/messages');
if (!res.ok) return;
const convs = await res.json();
renderConvList(convs);
} catch (e) { console.error(e); }
}
function renderConvList(convs) {
const list = document.getElementById('convList');
list.innerHTML = '';
if (convs.length === 0) {
list.innerHTML = '<li style="padding:1rem; color:var(--color-muted); font-size:0.9rem;">Noch keine Nachrichten. <a href="/search.html" style="color:var(--color-primary);">Personen suchen</a></li>';
return;
}
convs.forEach(c => {
const isLocation = c.partner.friendStatus === 'LOCATION';
const av = c.partner.profilePicture
? `<img src="data:image/png;base64,${c.partner.profilePicture}" alt="" style="cursor:zoom-in;" onclick="event.stopPropagation();openLightbox(this.src)">`
: (isLocation ? '📍' : '◉');
const unreadHtml = c.unreadCount > 0
? `<span class="conv-unread">${c.unreadCount}</span>`
: '';
const preview = c.lastMessage
? (c.lastMessage.text.startsWith('data:image/') ? '📷 Bild' : esc(c.lastMessage.text.substring(0, 40)))
: '';
const locationBadge = isLocation
? `<span style="font-size:0.6rem;background:var(--color-secondary);color:var(--color-muted);border-radius:4px;padding:0.1rem 0.3rem;margin-left:0.3rem;">Location</span>`
: '';
const li = document.createElement('li');
li.className = 'conv-item' + (c.partner.userId === activePartnerId ? ' active' : '');
li.dataset.partnerId = c.partner.userId;
li.dataset.isLocation = isLocation ? '1' : '';
li.innerHTML = `
<div class="conv-avatar">${av}</div>
<div class="conv-info">
<div class="conv-name">${esc(c.partner.name)}${locationBadge}</div>
<div class="conv-preview">${preview}</div>
</div>
${unreadHtml}`;
li.addEventListener('click', () => openThread(c.partner.userId, c.partner.name, c.partner.profilePicture, isLocation));
list.appendChild(li);
});
}
async function openThread(partnerId, partnerName, partnerPic, isLocation) {
activePartnerId = partnerId;
oldestSentAt = null;
newestSentAt = null;
hasMoreOlder = false;
isLoadingOlder = false;
document.querySelectorAll('.conv-item').forEach(li => {
li.classList.toggle('active', li.dataset.partnerId === partnerId);
});
// isLocation ggf. aus DOM ermitteln oder per API auflösen
if (isLocation === undefined) {
const convItem = document.querySelector(`.conv-item[data-partner-id="${partnerId}"]`);
if (convItem) {
isLocation = convItem.dataset.isLocation === '1';
} else {
// Noch kein Conv-Item (neue/unbekannte Konversation) → per API prüfen
try {
const locRes = await fetch('/locations/virtual/' + partnerId);
if (locRes.ok) {
isLocation = true;
const loc = await locRes.json();
if (!partnerName) partnerName = loc.name;
if (!partnerPic && loc.profilePictureLq) partnerPic = loc.profilePictureLq;
} else {
isLocation = false;
}
} catch { isLocation = false; }
}
}
if (!partnerName) {
const convItem = document.querySelector(`.conv-item[data-partner-id="${partnerId}"]`);
// .conv-name enthält ggf. das Location-Badge nur Textinhalt nehmen
partnerName = convItem ? convItem.querySelector('.conv-name').firstChild.textContent.trim() : '…';
}
const locationBadge = isLocation
? ` <span style="font-size:0.65rem;background:var(--color-secondary);color:var(--color-muted);border-radius:4px;padding:0.1rem 0.35rem;vertical-align:middle;">Location</span>`
: '';
if (isLocation) {
document.getElementById('threadPartnerName').innerHTML =
`${esc(partnerName)}${locationBadge}`;
} else {
document.getElementById('threadPartnerName').innerHTML =
`<a href="/community/benutzer.html?userId=${partnerId}" style="color:inherit;text-decoration:none;">${esc(partnerName)}</a>`;
}
const avatarEl = document.getElementById('threadPartnerAvatar');
if (partnerPic) {
avatarEl.innerHTML = `<img src="data:image/png;base64,${partnerPic}" alt="" style="cursor:zoom-in;" onclick="openLightbox(this.src)">`;
avatarEl.style.display = '';
} else if (isLocation) {
avatarEl.innerHTML = '📍';
avatarEl.style.display = '';
} else {
avatarEl.style.display = 'none';
}
const isSupport = partnerId === SUPPORT_USER_ID;
document.getElementById('threadInputWrap').style.display = isSupport ? 'none' : '';
if (!isSupport) document.getElementById('msgInput').focus();
if (window.innerWidth <= 768) showThread();
// Clear and load latest messages
const container = document.getElementById('threadMessages');
container.innerHTML = '';
await loadInitialThread();
startPolling();
}
async function loadInitialThread() {
if (!activePartnerId) return;
try {
const res = await fetch('/social/messages/' + activePartnerId);
if (!res.ok) return;
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;
}
// Zweifaches rAF stellt sicher, dass der Browser das Layout vollständig berechnet hat
// bevor gescrollt wird (wichtig bei Bild-Nachrichten)
requestAnimationFrame(() => requestAnimationFrame(() => {
container.scrollTop = container.scrollHeight;
}));
loadConversations();
} catch (e) { console.error(e); }
}
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');
const text = input.value.trim();
if (!text) return;
input.value = '';
try {
const res = await fetch('/social/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receiverId: activePartnerId, text })
});
if (res.status === 403) {
let reason = '';
try { const body = await res.json(); reason = body.reason; } catch (_) {}
if (reason === 'FIRST_MESSAGE_RESTRICTED') {
showThreadNotice('Du kannst diese Person nur anschreiben, wenn ihr befreundet seid, ein Match habt oder du ein Pro-Abo hast.');
} else if (reason === 'BLOCKED') {
showThreadNotice('Diese Konversation ist nicht mehr möglich.');
} else {
showThreadNotice('Nachricht konnte nicht gesendet werden.');
}
// Text wieder zurücksetzen, damit der User ihn nicht verliert
input.value = text;
return;
}
// War die Konversation leer, neu laden; sonst nur neue Nachrichten pollen
if (newestSentAt) {
await pollNewMessages();
} else {
await loadInitialThread();
}
} catch (e) { console.error(e); }
}
function showThreadNotice(msg) {
const existing = document.getElementById('threadNotice');
if (existing) existing.remove();
const notice = document.createElement('div');
notice.id = 'threadNotice';
notice.style.cssText = 'background:rgba(180,0,60,0.12);border:1px solid rgba(180,0,60,0.35);border-radius:8px;padding:0.75rem 1rem;font-size:0.88rem;color:var(--color-text);margin:0.5rem 0;line-height:1.45;';
notice.textContent = msg;
const container = document.getElementById('threadMessages');
container.appendChild(notice);
container.scrollTop = container.scrollHeight;
}
document.getElementById('msgInput').addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); }
});
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(pollNewMessages, 10000);
}
function showThread() {
document.getElementById('convListPane').classList.add('hidden');
document.getElementById('threadPane').classList.remove('hidden');
}
function showList() {
document.getElementById('convListPane').classList.remove('hidden');
document.getElementById('threadPane').classList.add('hidden');
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
activePartnerId = null;
}
function openLightbox(src) {
document.getElementById('lightboxImg').src = src;
document.getElementById('lightbox').classList.add('open');
}
function closeLightbox() {
document.getElementById('lightbox').classList.remove('open');
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeLightbox();
});
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
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 = [
'😀','😂','🤣','😅','😊','😍','🥰','😘','😎','🤩',
'😉','🙂','😐','😏','😒','😢','😭','😤','😡','🤬',
'🤔','🥺','😳','🤭','😈','💀','🙈','🙉','🙊','😴',
'👍','👎','👏','🙏','💪','🫶','🤗','🫠','✌️','🤞',
'❤️','🧡','💛','💚','💙','💜','🖤','💕','💖','💘',
'🔥','✨','⚡','🌟','💯','🎉','🎊','🎀','🌹','🌸',
'🍕','🍔','🍟','🌮','🍜','🍣','🍩','🍪','☕','🍷',
'🐱','🐶','🦊','🐼','🐨','🐸','🦄','🐝','🦋','🐬',
];
(function initEmojis() {
const picker = document.getElementById('emojiPicker');
EMOJIS.forEach(emoji => {
const btn = document.createElement('button');
btn.textContent = emoji;
btn.title = emoji;
btn.onclick = () => {
const input = document.getElementById('msgInput');
const pos = input.selectionStart ?? input.value.length;
const val = input.value;
input.value = val.slice(0, pos) + emoji + val.slice(pos);
input.selectionStart = input.selectionEnd = pos + [...emoji].length;
input.focus();
};
picker.appendChild(btn);
});
})();
function toggleEmoji() {
document.getElementById('emojiPicker').classList.toggle('open');
}
document.addEventListener('click', e => {
if (!e.target.closest('#emojiPicker') && !e.target.closest('#emojiBtn')) {
document.getElementById('emojiPicker').classList.remove('open');
}
});
// ── Bild-Upload ──
(function attachImgHandler() {
document.getElementById('imgFile').addEventListener('change', async function () {
const file = this.files[0];
if (!file || !activePartnerId) return;
const fresh = this.cloneNode(false);
this.replaceWith(fresh);
attachImgHandler();
try {
const dataUrl = await resizeImage(file);
await fetch('/social/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receiverId: activePartnerId, text: dataUrl })
});
await pollNewMessages();
} catch (e) { console.error(e); }
});
})();
function resizeImage(file) {
const MAX = 800;
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
let w = img.naturalWidth, h = img.naturalHeight;
if (w > MAX || h > MAX) {
if (w >= h) { h = Math.max(1, Math.round(MAX * h / w)); w = MAX; }
else { w = Math.max(1, Math.round(MAX * w / h)); h = MAX; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
resolve(canvas.toDataURL('image/jpeg', 0.72));
};
img.onerror = reject;
img.src = url;
});
}
</script>
</body>
</html>