noah33565 commited on
Commit
602f94d
Β·
verified Β·
1 Parent(s): e13097d

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +265 -34
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
- const msgs=getDMMsgs(key);
 
 
 
 
 
 
 
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
- const unread=unreadCounts['dm_'+u.username.toLowerCase()]||0;
 
 
 
 
 
 
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>${unread>0?`<span class="unread-badge">${unread}</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(\'tickets\',this)">Tickets</button>':''}
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-tickets" style="display:none">
1206
- <div class="settings-section"><h4>Ticket-System</h4><div id="admin-tickets-list">Wird geladen...</div></div>
1207
  </div>`:''}
1208
- <div class="modal-btns">
 
 
 
 
 
 
 
 
 
 
 
 
 
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==='tickets')loadAdminTickets();
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
- showModal(`<div class="modal-title">🎫 Ticket erstellen</div>
1260
- <div class="modal-sub">Wende dich an das Team!</div>
1261
- <div class="modal-field"><label>Kategorie</label>
1262
- <select id="t-cat"><option value="support">❓ Support</option><option value="report">🚨 Meldung</option><option value="appeal">βš–οΈ Ban-Appeal</option><option value="other">πŸ’­ Sonstiges</option></select></div>
1263
- <div class="modal-field"><label>Betreff</label><input type="text" id="t-title" maxlength="80"></div>
1264
- <div class="modal-field"><label>Beschreibung</label><textarea id="t-desc" rows="4"></textarea></div>
 
 
 
 
 
 
 
 
 
 
1265
  <div class="modal-btns">
1266
  <button class="modal-btn secondary" onclick="clearModal()">Abbrechen</button>
1267
- <button class="modal-btn primary" onclick="submitTicket()">Erstellen</button>
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
- dbPush('tickets',{id,cat,title,desc,author:currentUser.username,status:'open',ts:Date.now()});
1278
- clearModal();showToast(`🎫 Ticket ${id} erstellt`,'var(--green)');
 
 
 
 
 
 
 
 
 
 
 
1279
  }
1280
- function loadAdminTickets(){
1281
- const el=document.getElementById('admin-tickets-list');if(!el)return;
1282
- const tickets=Object.values(DB.tickets||{}).sort((a,b)=>b.ts-a.ts);
1283
- if(!tickets.length){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:20px;font-size:.85rem">Keine Tickets</div>';return;}
1284
- el.innerHTML=tickets.map(t=>`<div class="ticket-item ${t.status}" onclick="openTicketDetail('${escHtml(t._key||t.id)}')">
1285
- <div class="ticket-meta">${t.id} Β· ${t.cat} Β· ${new Date(t.ts).toLocaleDateString('de-DE')} Β· ${t.author}</div>
1286
- <div class="ticket-title">${escHtml(t.title)}</div>
1287
- <div style="font-size:.75rem;color:${t.status==='open'?'var(--green)':'var(--muted)'}">● ${t.status==='open'?'Offen':'Geschlossen'}</div>
1288
- </div>`).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1289
  }
 
1290
  function openTicketDetail(key){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1291
  const t=DB.tickets[key];if(!t)return;
1292
- const btnHtml = hasRole('mod')
1293
- ? `<div class="modal-btns"><button class="modal-btn secondary" onclick="clearModal()">Schließen</button><button class="modal-btn ${t.status==='open'?'danger-btn':'success-btn'}" onclick="toggleTicket('${key}')">${t.status==='open'?'Ticket schließen':'Ticket âffnen'}</button></div>`
1294
- : `<div class="modal-btns"><button class="modal-btn secondary" onclick="clearModal()">Schließen</button></div>`;
1295
- showModal(`<div class="modal-title">🎫 ${escHtml(t.id)}: ${escHtml(t.title)}</div><div class="modal-sub">Von ${escHtml(t.author)} · ${new Date(t.ts).toLocaleDateString('de-DE')} · ${t.status}</div><div style="background:rgba(0,0,0,0.3);padding:12px;border:1px solid var(--border);font-size:.9rem;margin-bottom:14px;border-radius:2px">${escHtml(t.desc)}</div>${btnHtml}`, true);
 
 
 
 
 
 
 
 
 
 
 
 
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>