Update app.js
Browse files
app.js
CHANGED
|
@@ -33,6 +33,21 @@ function loadSession() {
|
|
| 33 |
} catch { return null; }
|
| 34 |
}
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
// βββ STATE ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
let currentUser = null;
|
| 38 |
let currentChannel = 'allgemein';
|
|
@@ -68,6 +83,7 @@ const DB_DEFAULT = () => ({
|
|
| 68 |
voice: {},
|
| 69 |
tickets: {},
|
| 70 |
pins: [],
|
|
|
|
| 71 |
moderation: { bans:{}, banned_fps:{}, kicked:{}, timeouts:{} },
|
| 72 |
config: {}
|
| 73 |
});
|
|
@@ -180,6 +196,36 @@ function ensureFields() {
|
|
| 180 |
if (!DB.moderation.banned_fps) DB.moderation.banned_fps = {};
|
| 181 |
if (!DB.moderation.kicked) DB.moderation.kicked = {};
|
| 182 |
if (!DB.moderation.timeouts) DB.moderation.timeouts = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
// Hilfsfunktionen: Pfad-Zugriff (z.B. "users/noah")
|
|
@@ -543,6 +589,9 @@ function getDMMsgs(key) {
|
|
| 543 |
}
|
| 544 |
|
| 545 |
function openDM(username) {
|
|
|
|
|
|
|
|
|
|
| 546 |
currentDM=username; currentChannel=null;
|
| 547 |
document.querySelectorAll('.channel-item').forEach(el=>el.classList.remove('active'));
|
| 548 |
document.querySelectorAll('.dm-item').forEach(el=>{
|
|
@@ -555,14 +604,26 @@ function openDM(username) {
|
|
| 555 |
document.getElementById('send-btn').disabled=false;
|
| 556 |
document.getElementById('msg-input').placeholder=`Nachricht an @${username}...`;
|
| 557 |
unreadCounts['dm_'+username.toLowerCase()]=0;
|
|
|
|
|
|
|
|
|
|
| 558 |
updateDMList();
|
| 559 |
renderDMMessages(username);
|
| 560 |
}
|
| 561 |
|
| 562 |
function renderDMMessages(username) {
|
| 563 |
const key=getDMKey(currentUser.username,username);
|
| 564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
lastMsgCounts['dm_'+username.toLowerCase()]=msgs.length;
|
|
|
|
|
|
|
| 566 |
renderMessages(msgs,true);
|
| 567 |
}
|
| 568 |
|
|
@@ -617,7 +678,12 @@ function formatMsg(text) {
|
|
| 617 |
.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>')
|
| 618 |
.replace(/\*(.*?)\*/g,'<em>$1</em>')
|
| 619 |
.replace(/`(.*?)`/g,'<code style="background:rgba(0,212,255,0.08);padding:1px 5px;border-radius:2px;font-family:monospace">$1</code>')
|
| 620 |
-
.replace(/(https?:\/\/[^\s]+)/g,'<a href="$1" target="_blank" rel="noopener">$1</a>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
}
|
| 622 |
|
| 623 |
// βββ SEND βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -645,6 +711,11 @@ async function sendMessage() {
|
|
| 645 |
}
|
| 646 |
input.value=''; input.style.height='';
|
| 647 |
const msg={author:currentUser.username,color:currentUser.color,role:currentUser.role,text,ts:Date.now()};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
if (currentDM) {
|
| 649 |
const key=getDMKey(currentUser.username,currentDM);
|
| 650 |
if (!DB.dms[key]) DB.dms[key]={};
|
|
@@ -995,11 +1066,17 @@ function updateDMList() {
|
|
| 995 |
dmEl.innerHTML='';
|
| 996 |
for (const u of Object.values(DB.users)) {
|
| 997 |
if (u.username===currentUser?.username) continue;
|
| 998 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 999 |
const div=document.createElement('div');
|
| 1000 |
div.className=`channel-item dm-item ${currentDM===u.username?'active':''}`;
|
| 1001 |
div.dataset.user=u.username.toLowerCase();
|
| 1002 |
-
div.innerHTML=`<span class="channel-icon">@</span><span class="channel-name">${escHtml(u.username)}</span>${
|
| 1003 |
div.onclick=()=>openDM(u.username);
|
| 1004 |
dmEl.appendChild(div);
|
| 1005 |
}
|
|
@@ -1020,6 +1097,12 @@ function showUserCtx(e,username) {
|
|
| 1020 |
const h=document.createElement('div'); h.className='ctx-header'; h.textContent=username; menu.appendChild(h);
|
| 1021 |
addCtxItem(menu,'π¬ Direktnachricht','',()=>{hideCtx();openDM(username);});
|
| 1022 |
addCtxItem(menu,'π€ Profil','',()=>{hideCtx();showProfileModal(username);});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
if (hasRole('mod')) {
|
| 1024 |
addCtxDivider(menu);
|
| 1025 |
addCtxItem(menu,'π’ Kicken','warn',()=>{hideCtx();openKickModal(username);});
|
|
@@ -1040,6 +1123,15 @@ function addCtxDivider(menu){const d=document.createElement('div');d.className='
|
|
| 1040 |
function hideCtx(){document.getElementById('context-menu').style.display='none';}
|
| 1041 |
document.addEventListener('click',hideCtx);
|
| 1042 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1043 |
// βββ MODERATION βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1044 |
function openKickModal(u) {
|
| 1045 |
showModal(`<div class="modal-title">π’ Kick: ${escHtml(u)}</div>
|
|
@@ -1165,8 +1257,8 @@ function openSettings() {
|
|
| 1165 |
<button class="modal-tab active" onclick="switchSettingsTab('konto',this)">Konto</button>
|
| 1166 |
<button class="modal-tab" onclick="switchSettingsTab('darstellung',this)">Darstellung</button>
|
| 1167 |
${hasRole('mod')?'<button class="modal-tab" onclick="switchSettingsTab(\'automod\',this)">AutoMod</button>':''}
|
| 1168 |
-
${hasRole('admin')?'<button class="modal-tab" onclick="switchSettingsTab(\'
|
| 1169 |
-
</div>
|
| 1170 |
<div id="settings-tab-konto">
|
| 1171 |
<div class="settings-section"><h4>Konto</h4>
|
| 1172 |
<div class="modal-field"><label>Neuer Username</label><input type="text" id="s-user" placeholder="${escHtml(currentUser.username)}"></div>
|
|
@@ -1202,22 +1294,41 @@ function openSettings() {
|
|
| 1202 |
<div class="toggle-row"><label>Link-Filter</label><div class="toggle ${appConfig.automod?.linkFilter?'on':''}" id="toggle-links" onclick="toggleST('links')"></div></div>
|
| 1203 |
</div>
|
| 1204 |
</div>`:''}
|
| 1205 |
-
${hasRole('admin')?`<div id="settings-tab-
|
| 1206 |
-
<div class="settings-section"><h4>Ticket-
|
| 1207 |
</div>`:''}
|
| 1208 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
<button class="modal-btn secondary" onclick="clearModal()">SchlieΓen</button>
|
| 1210 |
<button class="modal-btn primary" onclick="saveSettings()">Speichern</button>
|
| 1211 |
</div>`,true);
|
| 1212 |
if (hasRole('admin')) setTimeout(loadAdminTickets,100);
|
| 1213 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1214 |
function toggleST(id){const el=document.getElementById('toggle-'+id);if(!el)return;const on=!el.classList.contains('on');el.classList.toggle('on',on);_st[id]=on;}
|
| 1215 |
function switchSettingsTab(tab,btn){
|
| 1216 |
document.querySelectorAll('[id^="settings-tab-"]').forEach(el=>el.style.display='none');
|
| 1217 |
document.querySelectorAll('.modal-tab').forEach(el=>el.classList.remove('active'));
|
| 1218 |
const el=document.getElementById('settings-tab-'+tab);if(el)el.style.display='';
|
| 1219 |
if(btn)btn.classList.add('active');
|
| 1220 |
-
if(tab==='
|
| 1221 |
}
|
| 1222 |
function addBW(){const i=document.getElementById('new-bw');const w=i.value.trim().toLowerCase();if(!w)return;if(!appConfig.automod)appConfig.automod={};if(!appConfig.automod.bannedWords)appConfig.automod.bannedWords=[];if(!appConfig.automod.bannedWords.includes(w)){appConfig.automod.bannedWords.push(w);document.getElementById('bw-tags').insertAdjacentHTML('beforeend',`<span class="tag">${escHtml(w)}<button class="tag-remove" onclick="removeBW('${escHtml(w)}')">Γ</button></span>`);}i.value='';}
|
| 1223 |
function removeBW(w){if(!appConfig.automod?.bannedWords)return;appConfig.automod.bannedWords=appConfig.automod.bannedWords.filter(x=>x!==w);const el=document.getElementById('bw-tags');if(el)el.innerHTML=appConfig.automod.bannedWords.map(x=>`<span class="tag">${escHtml(x)}<button class="tag-remove" onclick="removeBW('${escHtml(x)}')">Γ</button></span>`).join('');}
|
|
@@ -1256,43 +1367,151 @@ function saveSettings(){
|
|
| 1256 |
|
| 1257 |
// βββ TICKETS ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1258 |
function openCreateTicket(){
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
<div class="modal-
|
| 1262 |
-
|
| 1263 |
-
<div class="modal-field">
|
| 1264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1265 |
<div class="modal-btns">
|
| 1266 |
<button class="modal-btn secondary" onclick="clearModal()">Abbrechen</button>
|
| 1267 |
-
<button class="modal-btn primary" onclick="submitTicket()">
|
| 1268 |
-
</div>`);
|
| 1269 |
}
|
|
|
|
| 1270 |
function submitTicket(){
|
| 1271 |
const cat=document.getElementById('t-cat')?.value;
|
| 1272 |
const title=document.getElementById('t-title')?.value.trim();
|
| 1273 |
const desc=document.getElementById('t-desc')?.value.trim();
|
| 1274 |
if(!title||!desc){showToast('Bitte alle Felder ausfΓΌllen','var(--danger)');return;}
|
| 1275 |
-
const id='TKT-'+String(Date.now()).slice(-5);
|
| 1276 |
if(!DB.tickets)DB.tickets={};
|
| 1277 |
-
|
| 1278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1279 |
}
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
const
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
<div
|
| 1288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1289 |
}
|
|
|
|
| 1290 |
function openTicketDetail(key){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1291 |
const t=DB.tickets[key];if(!t)return;
|
| 1292 |
-
const
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1296 |
}
|
| 1297 |
|
| 1298 |
function toggleTicket(key){
|
|
@@ -1302,6 +1521,18 @@ function toggleTicket(key){
|
|
| 1302 |
showToast(`Ticket ${t.id} ${t.status==='open'?'geΓΆffnet':'geschlossen'}`,'var(--accent)');
|
| 1303 |
}
|
| 1304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
// βββ SEARCH & PINS ββββββββββββββββββββββββββββββββββββββββββββ
|
| 1306 |
async function openSearchModal(){
|
| 1307 |
showModal(`<div class="modal-title">π Nachrichten durchsuchen</div>
|
|
|
|
| 33 |
} catch { return null; }
|
| 34 |
}
|
| 35 |
|
| 36 |
+
// βββ DM READ TRACKING (persistent) βββββββββββββββββββββββββββ
|
| 37 |
+
function getDMReadTs(otherUser) {
|
| 38 |
+
try {
|
| 39 |
+
const data = JSON.parse(localStorage.getItem('nc_dm_read') || '{}');
|
| 40 |
+
return data[otherUser.toLowerCase()] || 0;
|
| 41 |
+
} catch { return 0; }
|
| 42 |
+
}
|
| 43 |
+
function setDMReadTs(otherUser, ts) {
|
| 44 |
+
try {
|
| 45 |
+
const data = JSON.parse(localStorage.getItem('nc_dm_read') || '{}');
|
| 46 |
+
data[otherUser.toLowerCase()] = ts;
|
| 47 |
+
localStorage.setItem('nc_dm_read', JSON.stringify(data));
|
| 48 |
+
} catch {}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
// βββ STATE ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
let currentUser = null;
|
| 53 |
let currentChannel = 'allgemein';
|
|
|
|
| 83 |
voice: {},
|
| 84 |
tickets: {},
|
| 85 |
pins: [],
|
| 86 |
+
blocks: {}, // { username: [blockedUsername, ...] }
|
| 87 |
moderation: { bans:{}, banned_fps:{}, kicked:{}, timeouts:{} },
|
| 88 |
config: {}
|
| 89 |
});
|
|
|
|
| 196 |
if (!DB.moderation.banned_fps) DB.moderation.banned_fps = {};
|
| 197 |
if (!DB.moderation.kicked) DB.moderation.kicked = {};
|
| 198 |
if (!DB.moderation.timeouts) DB.moderation.timeouts = {};
|
| 199 |
+
if (!DB.blocks) DB.blocks = {};
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// βββ BLOCK HELPERS ββββββββββββββββββββββββββββββββββββββββββββ
|
| 203 |
+
function isBlocked(username) {
|
| 204 |
+
const myBlocks = DB.blocks?.[currentUser?.username?.toLowerCase()] || [];
|
| 205 |
+
return myBlocks.includes(username.toLowerCase());
|
| 206 |
+
}
|
| 207 |
+
function isBlockedBy(username) {
|
| 208 |
+
const theirBlocks = DB.blocks?.[username.toLowerCase()] || [];
|
| 209 |
+
return theirBlocks.includes(currentUser?.username?.toLowerCase());
|
| 210 |
+
}
|
| 211 |
+
function blockUser(username) {
|
| 212 |
+
const key = currentUser.username.toLowerCase();
|
| 213 |
+
if (!DB.blocks[key]) DB.blocks[key] = [];
|
| 214 |
+
if (!DB.blocks[key].includes(username.toLowerCase())) {
|
| 215 |
+
DB.blocks[key].push(username.toLowerCase());
|
| 216 |
+
apiSave();
|
| 217 |
+
showToast(`π« ${username} blockiert`, 'var(--warn)');
|
| 218 |
+
if (currentDM === username) openChannel('allgemein');
|
| 219 |
+
updateDMList();
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
function unblockUser(username) {
|
| 223 |
+
const key = currentUser.username.toLowerCase();
|
| 224 |
+
if (!DB.blocks[key]) return;
|
| 225 |
+
DB.blocks[key] = DB.blocks[key].filter(u => u !== username.toLowerCase());
|
| 226 |
+
apiSave();
|
| 227 |
+
showToast(`β ${username} entblockt`, 'var(--green)');
|
| 228 |
+
updateDMList();
|
| 229 |
}
|
| 230 |
|
| 231 |
// Hilfsfunktionen: Pfad-Zugriff (z.B. "users/noah")
|
|
|
|
| 589 |
}
|
| 590 |
|
| 591 |
function openDM(username) {
|
| 592 |
+
// Blockiert?
|
| 593 |
+
if (isBlocked(username)) { showToast('Du hast diesen User blockiert.','var(--warn)'); return; }
|
| 594 |
+
if (isBlockedBy(username)) { showToast('Du kannst dieser Person keine Nachrichten senden.','var(--warn)'); return; }
|
| 595 |
currentDM=username; currentChannel=null;
|
| 596 |
document.querySelectorAll('.channel-item').forEach(el=>el.classList.remove('active'));
|
| 597 |
document.querySelectorAll('.dm-item').forEach(el=>{
|
|
|
|
| 604 |
document.getElementById('send-btn').disabled=false;
|
| 605 |
document.getElementById('msg-input').placeholder=`Nachricht an @${username}...`;
|
| 606 |
unreadCounts['dm_'+username.toLowerCase()]=0;
|
| 607 |
+
// Lesestand persistieren
|
| 608 |
+
const msgs=getDMMsgs(getDMKey(currentUser.username,username));
|
| 609 |
+
if (msgs.length) setDMReadTs(username, msgs[msgs.length-1].ts||0);
|
| 610 |
updateDMList();
|
| 611 |
renderDMMessages(username);
|
| 612 |
}
|
| 613 |
|
| 614 |
function renderDMMessages(username) {
|
| 615 |
const key=getDMKey(currentUser.username,username);
|
| 616 |
+
let msgs=getDMMsgs(key);
|
| 617 |
+
// Nachrichten von blockierten Usern ausblenden
|
| 618 |
+
if (isBlocked(username)||isBlockedBy(username)) {
|
| 619 |
+
document.getElementById('messages').innerHTML='<div class="empty-chat"><div class="empty-chat-icon">π«</div><p>Du kannst keine Nachrichten mit diesem User austauschen.</p></div>';
|
| 620 |
+
document.getElementById('msg-input').disabled=true;
|
| 621 |
+
document.getElementById('send-btn').disabled=true;
|
| 622 |
+
return;
|
| 623 |
+
}
|
| 624 |
lastMsgCounts['dm_'+username.toLowerCase()]=msgs.length;
|
| 625 |
+
// Lesestand beim Rendern aktualisieren
|
| 626 |
+
if (msgs.length) setDMReadTs(username, msgs[msgs.length-1].ts||0);
|
| 627 |
renderMessages(msgs,true);
|
| 628 |
}
|
| 629 |
|
|
|
|
| 678 |
.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>')
|
| 679 |
.replace(/\*(.*?)\*/g,'<em>$1</em>')
|
| 680 |
.replace(/`(.*?)`/g,'<code style="background:rgba(0,212,255,0.08);padding:1px 5px;border-radius:2px;font-family:monospace">$1</code>')
|
| 681 |
+
.replace(/(https?:\/\/[^\s]+)/g,'<a href="$1" target="_blank" rel="noopener">$1</a>')
|
| 682 |
+
.replace(/@(alle|here|everyone)/gi,'<span class="ping-everyone">@$1</span>')
|
| 683 |
+
.replace(/@([a-zA-Z0-9_]+)/g,(match,name)=>{
|
| 684 |
+
if(DB.users[name.toLowerCase()]) return `<span class="ping-user${name.toLowerCase()===currentUser?.username?.toLowerCase()?' ping-me':''}">${match}</span>`;
|
| 685 |
+
return match;
|
| 686 |
+
});
|
| 687 |
}
|
| 688 |
|
| 689 |
// βββ SEND βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 711 |
}
|
| 712 |
input.value=''; input.style.height='';
|
| 713 |
const msg={author:currentUser.username,color:currentUser.color,role:currentUser.role,text,ts:Date.now()};
|
| 714 |
+
|
| 715 |
+
// Ping-Erkennung fΓΌr Desktop-Notifications
|
| 716 |
+
const isPingAll = /@(alle|here|everyone)/i.test(text);
|
| 717 |
+
const isPingMe = new RegExp('@'+currentUser.username,'i').test(text);
|
| 718 |
+
|
| 719 |
if (currentDM) {
|
| 720 |
const key=getDMKey(currentUser.username,currentDM);
|
| 721 |
if (!DB.dms[key]) DB.dms[key]={};
|
|
|
|
| 1066 |
dmEl.innerHTML='';
|
| 1067 |
for (const u of Object.values(DB.users)) {
|
| 1068 |
if (u.username===currentUser?.username) continue;
|
| 1069 |
+
if (isBlocked(u.username)) continue; // Blockierte nicht anzeigen
|
| 1070 |
+
// Unread berechnen: Nachrichten neuer als letzter Lesestand
|
| 1071 |
+
const msgs=getDMMsgs(getDMKey(currentUser.username,u.username));
|
| 1072 |
+
const readTs=getDMReadTs(u.username);
|
| 1073 |
+
const unread=msgs.filter(m=>m.author!==currentUser.username&&(m.ts||0)>readTs).length;
|
| 1074 |
+
if (unread>0) unreadCounts['dm_'+u.username.toLowerCase()]=unread;
|
| 1075 |
+
const count=unreadCounts['dm_'+u.username.toLowerCase()]||0;
|
| 1076 |
const div=document.createElement('div');
|
| 1077 |
div.className=`channel-item dm-item ${currentDM===u.username?'active':''}`;
|
| 1078 |
div.dataset.user=u.username.toLowerCase();
|
| 1079 |
+
div.innerHTML=`<span class="channel-icon">@</span><span class="channel-name">${escHtml(u.username)}</span>${count>0?`<span class="unread-badge">${count}</span>`:''}`;
|
| 1080 |
div.onclick=()=>openDM(u.username);
|
| 1081 |
dmEl.appendChild(div);
|
| 1082 |
}
|
|
|
|
| 1097 |
const h=document.createElement('div'); h.className='ctx-header'; h.textContent=username; menu.appendChild(h);
|
| 1098 |
addCtxItem(menu,'π¬ Direktnachricht','',()=>{hideCtx();openDM(username);});
|
| 1099 |
addCtxItem(menu,'π€ Profil','',()=>{hideCtx();showProfileModal(username);});
|
| 1100 |
+
// Block/Unblock
|
| 1101 |
+
if (isBlocked(username)) {
|
| 1102 |
+
addCtxItem(menu,'β
Entblocken','',()=>{hideCtx();unblockUser(username);});
|
| 1103 |
+
} else {
|
| 1104 |
+
addCtxItem(menu,'π« Blockieren','warn',()=>{hideCtx();confirmBlock(username);});
|
| 1105 |
+
}
|
| 1106 |
if (hasRole('mod')) {
|
| 1107 |
addCtxDivider(menu);
|
| 1108 |
addCtxItem(menu,'π’ Kicken','warn',()=>{hideCtx();openKickModal(username);});
|
|
|
|
| 1123 |
function hideCtx(){document.getElementById('context-menu').style.display='none';}
|
| 1124 |
document.addEventListener('click',hideCtx);
|
| 1125 |
|
| 1126 |
+
function confirmBlock(username) {
|
| 1127 |
+
showModal(`<div class="modal-title">π« ${escHtml(username)} blockieren?</div>
|
| 1128 |
+
<div class="modal-sub" style="margin-bottom:16px">Du siehst keine DMs mehr von dieser Person und kannst ihr keine senden.</div>
|
| 1129 |
+
<div class="modal-btns">
|
| 1130 |
+
<button class="modal-btn secondary" onclick="clearModal()">Abbrechen</button>
|
| 1131 |
+
<button class="modal-btn warn-btn" onclick="blockUser('${escHtml(username)}');clearModal()">Blockieren</button>
|
| 1132 |
+
</div>`);
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
// βββ MODERATION βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1136 |
function openKickModal(u) {
|
| 1137 |
showModal(`<div class="modal-title">π’ Kick: ${escHtml(u)}</div>
|
|
|
|
| 1257 |
<button class="modal-tab active" onclick="switchSettingsTab('konto',this)">Konto</button>
|
| 1258 |
<button class="modal-tab" onclick="switchSettingsTab('darstellung',this)">Darstellung</button>
|
| 1259 |
${hasRole('mod')?'<button class="modal-tab" onclick="switchSettingsTab(\'automod\',this)">AutoMod</button>':''}
|
| 1260 |
+
${hasRole('admin')?'<button class="modal-tab" onclick="switchSettingsTab(\'admin\',this)">Admin</button>':''}
|
| 1261 |
+
<button class="modal-tab" onclick="switchSettingsTab(\'blockiert\',this)">Blockiert</button> </div>
|
| 1262 |
<div id="settings-tab-konto">
|
| 1263 |
<div class="settings-section"><h4>Konto</h4>
|
| 1264 |
<div class="modal-field"><label>Neuer Username</label><input type="text" id="s-user" placeholder="${escHtml(currentUser.username)}"></div>
|
|
|
|
| 1294 |
<div class="toggle-row"><label>Link-Filter</label><div class="toggle ${appConfig.automod?.linkFilter?'on':''}" id="toggle-links" onclick="toggleST('links')"></div></div>
|
| 1295 |
</div>
|
| 1296 |
</div>`:''}
|
| 1297 |
+
${hasRole('admin')?`<div id="settings-tab-admin" style="display:none">
|
| 1298 |
+
<div class="settings-section"><h4>Ticket-Γbersicht</h4><div id="admin-tickets-list">Wird geladen...</div></div>
|
| 1299 |
</div>`:''}
|
| 1300 |
+
<div id="settings-tab-blockiert" style="display:none">
|
| 1301 |
+
<div class="settings-section"><h4>Blockierte User</h4>
|
| 1302 |
+
<div id="blocked-list">
|
| 1303 |
+
${(()=>{
|
| 1304 |
+
const bl=DB.blocks?.[currentUser.username.toLowerCase()]||[];
|
| 1305 |
+
if(!bl.length) return '<div style="color:var(--muted);font-size:.85rem;padding:12px 0">Niemand blockiert</div>';
|
| 1306 |
+
return bl.map(u=>`<div style="display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid var(--border)">
|
| 1307 |
+
<span>${escHtml(u)}</span>
|
| 1308 |
+
<button class="modal-btn secondary" style="padding:3px 10px;font-size:.7rem;width:auto" onclick="unblockUser('${escHtml(u)}');this.closest('.settings-section').parentElement.parentElement.querySelector('#blocked-list').innerHTML='<div style=color:var(--muted);font-size:.85rem;padding:12px 0>Aktualisiert β bitte Tab neu ΓΆffnen</div>'">Entblocken</button>
|
| 1309 |
+
</div>`).join('');
|
| 1310 |
+
})()}
|
| 1311 |
+
</div>
|
| 1312 |
+
</div>
|
| 1313 |
+
</div> <div class="modal-btns">
|
| 1314 |
<button class="modal-btn secondary" onclick="clearModal()">SchlieΓen</button>
|
| 1315 |
<button class="modal-btn primary" onclick="saveSettings()">Speichern</button>
|
| 1316 |
</div>`,true);
|
| 1317 |
if (hasRole('admin')) setTimeout(loadAdminTickets,100);
|
| 1318 |
}
|
| 1319 |
+
function loadAdminTickets(){
|
| 1320 |
+
const el=document.getElementById('admin-tickets-list');if(!el)return;
|
| 1321 |
+
const tickets=Object.values(DB.tickets||{}).sort((a,b)=>b.ts-a.ts);
|
| 1322 |
+
if(!tickets.length){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:20px;font-size:.85rem">Keine Tickets</div>';return;}
|
| 1323 |
+
el.innerHTML=renderTicketList(tickets);
|
| 1324 |
+
}
|
| 1325 |
function toggleST(id){const el=document.getElementById('toggle-'+id);if(!el)return;const on=!el.classList.contains('on');el.classList.toggle('on',on);_st[id]=on;}
|
| 1326 |
function switchSettingsTab(tab,btn){
|
| 1327 |
document.querySelectorAll('[id^="settings-tab-"]').forEach(el=>el.style.display='none');
|
| 1328 |
document.querySelectorAll('.modal-tab').forEach(el=>el.classList.remove('active'));
|
| 1329 |
const el=document.getElementById('settings-tab-'+tab);if(el)el.style.display='';
|
| 1330 |
if(btn)btn.classList.add('active');
|
| 1331 |
+
if(tab==='admin')loadAdminTickets();
|
| 1332 |
}
|
| 1333 |
function addBW(){const i=document.getElementById('new-bw');const w=i.value.trim().toLowerCase();if(!w)return;if(!appConfig.automod)appConfig.automod={};if(!appConfig.automod.bannedWords)appConfig.automod.bannedWords=[];if(!appConfig.automod.bannedWords.includes(w)){appConfig.automod.bannedWords.push(w);document.getElementById('bw-tags').insertAdjacentHTML('beforeend',`<span class="tag">${escHtml(w)}<button class="tag-remove" onclick="removeBW('${escHtml(w)}')">Γ</button></span>`);}i.value='';}
|
| 1334 |
function removeBW(w){if(!appConfig.automod?.bannedWords)return;appConfig.automod.bannedWords=appConfig.automod.bannedWords.filter(x=>x!==w);const el=document.getElementById('bw-tags');if(el)el.innerHTML=appConfig.automod.bannedWords.map(x=>`<span class="tag">${escHtml(x)}<button class="tag-remove" onclick="removeBW('${escHtml(x)}')">Γ</button></span>`).join('');}
|
|
|
|
| 1367 |
|
| 1368 |
// βββ TICKETS ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1369 |
function openCreateTicket(){
|
| 1370 |
+
if (!currentUser) return;
|
| 1371 |
+
showModal(`
|
| 1372 |
+
<div class="modal-title">π« Ticket erstellen</div>
|
| 1373 |
+
<div class="modal-sub">Das Team wird sich so schnell wie mΓΆglich melden.</div>
|
| 1374 |
+
<div class="modal-field">
|
| 1375 |
+
<label>Kategorie</label>
|
| 1376 |
+
<select id="t-cat">
|
| 1377 |
+
<option value="support">β Support</option>
|
| 1378 |
+
<option value="report">π¨ Meldung</option>
|
| 1379 |
+
<option value="appeal">βοΈ Ban-Appeal</option>
|
| 1380 |
+
<option value="bug">π Bug-Report</option>
|
| 1381 |
+
<option value="other">π Sonstiges</option>
|
| 1382 |
+
</select>
|
| 1383 |
+
</div>
|
| 1384 |
+
<div class="modal-field"><label>Betreff</label><input type="text" id="t-title" maxlength="80" placeholder="Kurze Beschreibung"></div>
|
| 1385 |
+
<div class="modal-field"><label>Beschreibung</label><textarea id="t-desc" rows="4" placeholder="Beschreibe dein Anliegen genau..."></textarea></div>
|
| 1386 |
<div class="modal-btns">
|
| 1387 |
<button class="modal-btn secondary" onclick="clearModal()">Abbrechen</button>
|
| 1388 |
+
<button class="modal-btn primary" onclick="submitTicket()">Ticket erstellen βΆ</button>
|
| 1389 |
+
</div>`, true);
|
| 1390 |
}
|
| 1391 |
+
|
| 1392 |
function submitTicket(){
|
| 1393 |
const cat=document.getElementById('t-cat')?.value;
|
| 1394 |
const title=document.getElementById('t-title')?.value.trim();
|
| 1395 |
const desc=document.getElementById('t-desc')?.value.trim();
|
| 1396 |
if(!title||!desc){showToast('Bitte alle Felder ausfΓΌllen','var(--danger)');return;}
|
|
|
|
| 1397 |
if(!DB.tickets)DB.tickets={};
|
| 1398 |
+
const tktNum = Object.keys(DB.tickets).length + 1;
|
| 1399 |
+
const id='TKT-'+String(tktNum).padStart(4,'0');
|
| 1400 |
+
dbPush('tickets',{
|
| 1401 |
+
id, cat, title, desc,
|
| 1402 |
+
author: currentUser.username,
|
| 1403 |
+
status: 'open',
|
| 1404 |
+
ts: Date.now(),
|
| 1405 |
+
replies: []
|
| 1406 |
+
});
|
| 1407 |
+
clearModal();
|
| 1408 |
+
showToast(`β
Ticket ${id} erstellt! Das Team meldet sich.`,'var(--green)');
|
| 1409 |
+
// Mod-Notification
|
| 1410 |
+
sendSystemMsg(`π¬ Neues Ticket von @${currentUser.username}: "${title}" (${id})`, 'moderators');
|
| 1411 |
}
|
| 1412 |
+
|
| 1413 |
+
function openMyTickets(){
|
| 1414 |
+
const myTickets = Object.values(DB.tickets||{})
|
| 1415 |
+
.filter(t=>t.author===currentUser.username||hasRole('mod'))
|
| 1416 |
+
.sort((a,b)=>b.ts-a.ts);
|
| 1417 |
+
|
| 1418 |
+
showModal(`
|
| 1419 |
+
<div class="modal-title">π« ${hasRole('mod')?'Alle Tickets':'Meine Tickets'}</div>
|
| 1420 |
+
<div style="display:flex;gap:8px;margin-bottom:12px">
|
| 1421 |
+
<button class="modal-btn secondary" style="flex:1;padding:5px" onclick="filterTickets('all',this)">Alle</button>
|
| 1422 |
+
<button class="modal-btn secondary" style="flex:1;padding:5px" onclick="filterTickets('open',this)">Offen</button>
|
| 1423 |
+
<button class="modal-btn secondary" style="flex:1;padding:5px" onclick="filterTickets('closed',this)">Geschlossen</button>
|
| 1424 |
+
</div>
|
| 1425 |
+
<div id="ticket-list-container" style="max-height:380px;overflow-y:auto">
|
| 1426 |
+
${renderTicketList(myTickets)}
|
| 1427 |
+
</div>
|
| 1428 |
+
<div class="modal-btns">
|
| 1429 |
+
<button class="modal-btn secondary" onclick="clearModal()">SchlieΓen</button>
|
| 1430 |
+
<button class="modal-btn primary" onclick="clearModal();openCreateTicket()">+ Neues Ticket</button>
|
| 1431 |
+
</div>`, true);
|
| 1432 |
+
}
|
| 1433 |
+
|
| 1434 |
+
function renderTicketList(tickets){
|
| 1435 |
+
if(!tickets.length) return '<div style="color:var(--muted);text-align:center;padding:24px;font-size:.85rem">Keine Tickets</div>';
|
| 1436 |
+
const catIcon={support:'β',report:'π¨',appeal:'βοΈ',bug:'π',other:'π'};
|
| 1437 |
+
return tickets.map(t=>`
|
| 1438 |
+
<div class="ticket-item ${t.status}" onclick="openTicketDetail('${t._key||t.id}')" style="cursor:pointer;padding:10px 12px;border:1px solid var(--border);border-radius:4px;margin-bottom:8px;background:var(--surface2);transition:border-color .2s" onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
|
| 1439 |
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
| 1440 |
+
<span style="font-family:Orbitron,monospace;font-size:.65rem;color:var(--accent)">${t.id}</span>
|
| 1441 |
+
<span style="font-size:.65rem;padding:2px 7px;border-radius:2px;background:${t.status==='open'?'rgba(78,255,145,0.15)':'rgba(74,122,153,0.2)'};color:${t.status==='open'?'var(--green)':'var(--muted)'}">β ${t.status==='open'?'OFFEN':'GESCHLOSSEN'}</span>
|
| 1442 |
+
</div>
|
| 1443 |
+
<div style="font-size:.9rem;font-weight:700;margin-bottom:2px">${catIcon[t.cat]||'π'} ${escHtml(t.title)}</div>
|
| 1444 |
+
<div style="font-size:.72rem;color:var(--muted)">${t.author} Β· ${new Date(t.ts).toLocaleDateString('de-DE')} Β· ${(t.replies||[]).length} Antwort${(t.replies||[]).length!==1?'en':''}</div>
|
| 1445 |
+
</div>`).join('');
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
function filterTickets(filter, btn){
|
| 1449 |
+
document.querySelectorAll('#active-modal .modal-btn.secondary').forEach(b=>b.classList.remove('active'));
|
| 1450 |
+
btn?.classList.add('active');
|
| 1451 |
+
const all = Object.values(DB.tickets||{})
|
| 1452 |
+
.filter(t=>t.author===currentUser.username||hasRole('mod'))
|
| 1453 |
+
.filter(t=>filter==='all'||t.status===filter)
|
| 1454 |
+
.sort((a,b)=>b.ts-a.ts);
|
| 1455 |
+
const container = document.getElementById('ticket-list-container');
|
| 1456 |
+
if(container) container.innerHTML = renderTicketList(all);
|
| 1457 |
}
|
| 1458 |
+
|
| 1459 |
function openTicketDetail(key){
|
| 1460 |
+
// Key kann entweder _key oder id sein
|
| 1461 |
+
const t = DB.tickets[key] || Object.values(DB.tickets||{}).find(x=>x.id===key);
|
| 1462 |
+
if(!t) return showToast('Ticket nicht gefunden','var(--danger)');
|
| 1463 |
+
const realKey = t._key || key;
|
| 1464 |
+
const catIcon={support:'β',report:'π¨',appeal:'βοΈ',bug:'π',other:'π'};
|
| 1465 |
+
const replies = (t.replies||[]).map(r=>`
|
| 1466 |
+
<div style="padding:10px;border-left:2px solid ${r.isStaff?'var(--accent)':'var(--border)'};margin:8px 0;background:rgba(0,0,0,0.2);border-radius:0 4px 4px 0">
|
| 1467 |
+
<div style="font-size:.68rem;color:${r.isStaff?'var(--accent)':'var(--muted)'};font-family:Orbitron,monospace;margin-bottom:4px">
|
| 1468 |
+
${r.isStaff?'π‘ ':''}${escHtml(r.author)} Β· ${new Date(r.ts).toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'})}
|
| 1469 |
+
</div>
|
| 1470 |
+
<div style="font-size:.88rem">${escHtml(r.text)}</div>
|
| 1471 |
+
</div>`).join('');
|
| 1472 |
+
|
| 1473 |
+
const canReply = t.author===currentUser.username || hasRole('mod');
|
| 1474 |
+
const canClose = hasRole('mod') || t.author===currentUser.username;
|
| 1475 |
+
|
| 1476 |
+
showModal(`
|
| 1477 |
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px">
|
| 1478 |
+
<div>
|
| 1479 |
+
<div class="modal-title" style="margin-bottom:2px">${catIcon[t.cat]||'π'} ${escHtml(t.title)}</div>
|
| 1480 |
+
<div style="font-family:Orbitron,monospace;font-size:.62rem;color:var(--accent)">${t.id} Β· ${t.author} Β· ${new Date(t.ts).toLocaleDateString('de-DE')}</div>
|
| 1481 |
+
</div>
|
| 1482 |
+
<span style="font-size:.65rem;padding:3px 8px;border-radius:2px;white-space:nowrap;background:${t.status==='open'?'rgba(78,255,145,0.15)':'rgba(74,122,153,0.2)'};color:${t.status==='open'?'var(--green)':'var(--muted)'}">β ${t.status==='open'?'OFFEN':'GESCHLOSSEN'}</span>
|
| 1483 |
+
</div>
|
| 1484 |
+
<div style="background:rgba(0,0,0,0.3);padding:12px;border:1px solid var(--border);font-size:.9rem;margin-bottom:12px;border-radius:4px;line-height:1.5">${escHtml(t.desc)}</div>
|
| 1485 |
+
${replies?`<div style="max-height:200px;overflow-y:auto;margin-bottom:12px">${replies}</div>`:''}
|
| 1486 |
+
${canReply&&t.status==='open'?`
|
| 1487 |
+
<div class="modal-field" style="margin-bottom:8px">
|
| 1488 |
+
<textarea id="ticket-reply" rows="2" placeholder="Antwort schreiben..." style="font-size:.88rem"></textarea>
|
| 1489 |
+
</div>`:''}
|
| 1490 |
+
<div class="modal-btns">
|
| 1491 |
+
<button class="modal-btn secondary" onclick="clearModal()">SchlieΓen</button>
|
| 1492 |
+
${canReply&&t.status==='open'?`<button class="modal-btn primary" onclick="replyTicket('${realKey}')">Antworten</button>`:''}
|
| 1493 |
+
${canClose?`<button class="modal-btn ${t.status==='open'?'warn-btn':'success-btn'}" onclick="toggleTicket('${realKey}')">${t.status==='open'?'SchlieΓen':'Wieder ΓΆffnen'}</button>`:''}
|
| 1494 |
+
</div>`, true);
|
| 1495 |
+
}
|
| 1496 |
+
|
| 1497 |
+
function replyTicket(key){
|
| 1498 |
const t=DB.tickets[key];if(!t)return;
|
| 1499 |
+
const text=document.getElementById('ticket-reply')?.value.trim();
|
| 1500 |
+
if(!text){showToast('Antwort darf nicht leer sein','var(--danger)');return;}
|
| 1501 |
+
if(!t.replies)t.replies=[];
|
| 1502 |
+
t.replies.push({
|
| 1503 |
+
author: currentUser.username,
|
| 1504 |
+
text,
|
| 1505 |
+
ts: Date.now(),
|
| 1506 |
+
isStaff: hasRole('mod')
|
| 1507 |
+
});
|
| 1508 |
+
apiSave();
|
| 1509 |
+
clearModal();
|
| 1510 |
+
showToast('β
Antwort gesendet','var(--green)');
|
| 1511 |
+
// Notification an Ticket-Ersteller
|
| 1512 |
+
if(t.author!==currentUser.username){
|
| 1513 |
+
sendSystemMsg(`π¬ Neue Antwort auf Ticket ${t.id} von @${currentUser.username}`, 'moderators');
|
| 1514 |
+
}
|
| 1515 |
}
|
| 1516 |
|
| 1517 |
function toggleTicket(key){
|
|
|
|
| 1521 |
showToast(`Ticket ${t.id} ${t.status==='open'?'geΓΆffnet':'geschlossen'}`,'var(--accent)');
|
| 1522 |
}
|
| 1523 |
|
| 1524 |
+
function sendSystemMsg(text, channelId){
|
| 1525 |
+
if(!DB.msgs) DB.msgs={};
|
| 1526 |
+
if(!DB.msgs[channelId]) DB.msgs[channelId]={};
|
| 1527 |
+
dbPush(`msgs/${channelId}`,{system:true,text,ts:Date.now()});
|
| 1528 |
+
}
|
| 1529 |
+
|
| 1530 |
+
// Ticket-Button auch fΓΌr normale User sichtbar machen (meine Tickets)
|
| 1531 |
+
function openTicketMenu(){
|
| 1532 |
+
if(hasRole('mod')) return openMyTickets();
|
| 1533 |
+
openMyTickets();
|
| 1534 |
+
}
|
| 1535 |
+
|
| 1536 |
// βββ SEARCH & PINS ββββββββββββββββββββββββββββββββββββββββββββ
|
| 1537 |
async function openSearchModal(){
|
| 1538 |
showModal(`<div class="modal-title">π Nachrichten durchsuchen</div>
|