1/* CH WP Chat by Conny Henn - https://blog.hennweb.de/ch-wp-chat/ */ 2(() => { 3 'use strict'; 4 const $$ = (s, r=document) => [...r.querySelectorAll(s)]; 5 const configOf = el => { try { return JSON.parse(el.dataset.config || '{}'); } catch (_) { return {}; } }; 6 const SESSION_KEY='chfc_session', SOUND_KEY='chfc_sound_enabled', INVITE_SOUND_KEY='chfc_invite_sound_enabled', BELL_RECEIVE_KEY='chfc_bell_receive_enabled', FAVORITES_KEY='chfc_favorites', RECENT_EMOJI_KEY='chfc_recent_emoji', DENSITY_KEY='chfc_display_density', OWN_LEFT_KEY='chfc_own_left'; 7 8 class Api { 9 constructor(config){ this.config=config; } 10 session(){ try{return JSON.parse(localStorage.getItem(SESSION_KEY)||'null');}catch(_){return null;} } 11 save(v){ v?localStorage.setItem(SESSION_KEY,JSON.stringify(v)):localStorage.removeItem(SESSION_KEY); } 12 async call(path, options={}){ 13 const session=this.session(), isForm=options.body instanceof FormData, headers={...(isForm?{}:{'Content-Type':'application/json'}),...(options.headers||{})}; 14 const url=new URL(this.config.restUrl+path,location.href); 15 if(session){ headers['X-CHFC-Token']=session.token; url.searchParams.set('session_id',session.session_id); } 16 const response=await fetch(url,{...options,headers,credentials:'same-origin',cache:'no-store'}); 17 const data=await response.json().catch(()=>({})); 18 if(!response.ok) throw new Error(data.message||'Unbekannter Fehler.'); 19 return data; 20 } 21 } 22 23 class App { 24 constructor(root){ 25 this.root=root; this.config=configOf(root); this.api=new Api(this.config); 26 this.login=root.querySelector('[data-chfc-login]'); this.roomEl=root.querySelector('[data-chfc-room]'); 27 this.loginForm=root.querySelector('[data-action="join"]'); this.loginError=root.querySelector('[data-login-error]'); 28 this.room=null; this.loginForm.addEventListener('submit',e=>this.join(e)); 29 $$('[data-login-dialog]',this.login).forEach(b=>b.addEventListener('click',()=>this.login.querySelector(`[data-login-info-dialog="${b.dataset.loginDialog}"]`)?.showModal())); 30 $$('[data-action="close-login-info"]',this.login).forEach(b=>b.addEventListener('click',()=>b.closest('dialog')?.close())); 31 this.api.session()?this.showRoom():this.showLogin(); 32 } 33 showLogin(message=''){ 34 if(this.room)this.room.destroy(); this.room=null; this.roomEl.hidden=true; this.login.hidden=false; 35 this.loginError.textContent=message; setTimeout(()=>this.loginForm.elements.nickname?.focus(),60); 36 } 37 showRoom(){ this.login.hidden=true; this.roomEl.hidden=false; this.room=new Room(this.roomEl,this.config,this.api,m=>this.showLogin(m)); } 38 async join(e){ 39 e.preventDefault(); if(!this.loginForm.reportValidity())return; 40 const submit=this.loginForm.querySelector('button[type="submit"]'), formData=new FormData(this.loginForm), nickname=String(formData.get('nickname')||'').trim(), gravatar_email=String(formData.get('gravatar_email')||'').trim(); 41 submit.disabled=true; this.loginError.textContent=''; 42 try{ const data=await this.api.call('/join',{method:'POST',body:JSON.stringify({nickname,gravatar_email})}); this.api.save(data); this.loginForm.reset(); this.showRoom(); } 43 catch(err){ this.loginError.textContent=err.message; } 44 finally{ submit.disabled=false; } 45 } 46 } 47 48 class Room { 49 constructor(root,config,api,onLogout){ 50 this.root=root; this.config=config; this.api=api; this.onLogout=onLogout; this.session=api.session(); 51 this.messages=root.querySelector('[data-messages]'); this.tabs=root.querySelector('[data-chat-tabs]'); this.newMessagesButton=root.querySelector('[data-action="new-messages"]'); this.users=root.querySelector('[data-users]'); 52 this.error=root.querySelector('[data-error]'); this.typingEl=root.querySelector('[data-typing]'); this.form=root.querySelector('[data-action="send"]'); this.textarea=this.form.elements.message; 53 this.current={id:'',name:'Ãffentlicher Chat'}; this.last={public:0,private:{}}; this.privateChats=new Map(); this.unread=new Map(); 54 this.busy=false; this.sending=false; this.initial=true; this.pendingNew=0; this.lastRendered=null; this.renderedMessageIds=new Set(); this.timer=null; this.currentInvite=null; this.seenInvites=new Set(); this.acceptedSeen=new Set(); 55 this.sound=localStorage.getItem(SOUND_KEY)!=='0'; this.inviteSound=localStorage.getItem(INVITE_SOUND_KEY)!=='0'; this.bellReceive=localStorage.getItem(BELL_RECEIVE_KEY)!=='0'; this.audio=null; this.ringtoneAudio=null; this.ringtoneUnlocked=false; this.userItems=[]; this.searchTerm=''; 56 this.messageFilter=''; this.messageFilterInput=root.querySelector('[data-message-filter]'); this.resetFilterButton=root.querySelector('[data-action="reset-filter"]'); 57 this.favorites=new Set(this.readJson(FAVORITES_KEY,[])); this.recentEmoji=this.readJson(RECENT_EMOJI_KEY,[]); 58 this.typingTimer=null; this.typingSent=false; this.mentionIndex=0; this.mentionMatches=[]; 59 this.displayDensity=localStorage.getItem(DENSITY_KEY)||config.displayDensity||'comfortable'; 60 if(!['comfortable','compact','dense'].includes(this.displayDensity))this.displayDensity='comfortable'; 61 this.ownLeft=localStorage.getItem(OWN_LEFT_KEY)==='1'; 62 this.applyDisplayPreferences(); 63 this.bind(); this.buildEmoji(); this.renderChatTabs(); this.updateSound(); this.updateInviteSound(); this.updateBellReceive(); this.updateChars(); this.tick(); 64 this.timer=setInterval(()=>this.tick(),Math.max(2,Number(config.pollSeconds||4))*1000); 65 } 66 readJson(key,fallback){try{return JSON.parse(localStorage.getItem(key)||JSON.stringify(fallback));}catch(_){return fallback;}} 67 bind(){ 68 this.form.addEventListener('submit',e=>this.send(e)); 69 this.textarea.addEventListener('input',()=>{this.updateChars();this.handleTyping();this.updateMentionMenu();}); 70 this.textarea.addEventListener('keydown',e=>this.handleKeydown(e)); 71 this.root.querySelector('[data-action="public"]').addEventListener('click',()=>this.openPublic()); 72 this.root.querySelector('[data-action="leave"]').addEventListener('click',()=>this.leave()); 73 this.root.querySelector('[data-action="clear-for-me"]')?.addEventListener('click',()=>this.clearForMe()); 74 $$('[data-action="sound"]',this.root).forEach(b=>b.addEventListener('click',()=>this.toggleSound())); 75 $$('[data-action="invite-sound"]',this.root).forEach(b=>b.addEventListener('click',()=>this.toggleInviteSound())); 76 $$('[data-action="bell-receive"]',this.root).forEach(b=>b.addEventListener('click',()=>this.toggleBellReceive())); 77 this.root.querySelector('[data-action="emoji"]').addEventListener('click',e=>this.toggleEmoji(e));
78 const choose=this.root.querySelector('[data-action="choose-image"]'), imageInput=this.root.querySelector('[data-image-input]'); 79 if(choose&&imageInput){choose.addEventListener('click',()=>imageInput.click());imageInput.addEventListener('change',()=>this.uploadImage(imageInput));this.textarea.addEventListener('paste',e=>this.handleClipboardPaste(e));} 80 this.root.querySelector('[data-emoji-search]').addEventListener('input',e=>this.filterEmoji(e.target.value)); 81 this.root.querySelector('[data-user-search]').addEventListener('input',e=>{this.searchTerm=e.target.value.trim().toLocaleLowerCase();this.renderUsers(this.userItems);}); 82 this.messageFilterInput.addEventListener('input',e=>{this.messageFilter=e.target.value.trim().toLocaleLowerCase();this.resetFilterButton.disabled=!this.messageFilter;this.applyMessageFilter();}); 83 this.resetFilterButton.addEventListener('click',()=>this.resetMessageFilter()); 84 this.root.querySelector('[data-action="apply-filter"]')?.addEventListener('click',()=>{this.messageFilter=this.messageFilterInput.value.trim().toLocaleLowerCase();this.resetFilterButton.disabled=!this.messageFilter;this.applyMessageFilter();this.messageFilterInput.focus();}); 85 this.root.querySelector('[data-action="jump-latest"]').addEventListener('click',()=>this.jumpToLatest()); 86 this.root.querySelector('[data-action="ring-user"]')?.addEventListener('click',()=>this.ringCurrentUser()); 87 this.newMessagesButton.addEventListener('click',()=>this.jumpToLatest()); 88 this.messages.addEventListener('scroll',()=>{if(this.isNearBottom())this.clearNewMessageNotice();}); 89 this.root.querySelector('[data-action="invite-accept"]').addEventListener('click',()=>this.respondInvite('accepted')); 90 this.root.querySelector('[data-action="invite-decline"]').addEventListener('click',()=>this.respondInvite('declined')); 91 const menuButton=this.root.querySelector('[data-action="menu"]'),menu=this.root.querySelector('[data-menu]'); 92 menuButton?.addEventListener('click',e=>{e.stopPropagation();const open=menu.hidden;menu.hidden=!open;menuButton.setAttribute('aria-expanded',String(open));}); 93 $$('[data-dialog]',menu).forEach(b=>b.addEventListener('click',()=>{this.openInfoDialog(b.dataset.dialog);menu.hidden=true;menuButton.setAttribute('aria-expanded','false');})); 94 $$('[data-action="close-info"]',this.root).forEach(b=>b.addEventListener('click',()=>b.closest('dialog')?.close())); 95 const density=this.root.querySelector('[data-display-density]'),ownLeft=this.root.querySelector('[data-own-left]'); 96 if(density){density.value=this.displayDensity;density.addEventListener('change',()=>{this.displayDensity=density.value;localStorage.setItem(DENSITY_KEY,this.displayDensity);this.applyDisplayPreferences();});} 97 if(ownLeft){ownLeft.checked=this.ownLeft;ownLeft.addEventListener('change',()=>{this.ownLeft=ownLeft.checked;localStorage.setItem(OWN_LEFT_KEY,this.ownLeft?'1':'0');this.applyDisplayPreferences();});} 98 this.unlockRingtone=()=>this.unlockCustomRingtone(); 99 document.addEventListener('pointerdown',this.unlockRingtone,{once:true}); 100 document.addEventListener('keydown',this.unlockRingtone,{once:true}); 101 this.outside=e=>{if(!e.target.closest('.chfc-compose-box')){this.closeEmoji();this.closeMentionMenu();}const menu=this.root.querySelector('[data-menu]');if(menu&&!menu.hidden&&!e.target.closest('.chfc-menu-wrap')){menu.hidden=true;this.root.querySelector('[data-action="menu"]')?.setAttribute('aria-expanded','false');}}; document.addEventListener('click',this.outside); 102 } 103 applyDisplayPreferences(){ 104 this.root.classList.remove('chfc-density-comfortable','chfc-density-compact','chfc-density-dense','chfc-own-left'); 105 this.root.classList.add(`chfc-density-${this.displayDensity}`); 106 this.root.classList.toggle('chfc-own-left',this.ownLeft); 107 } 108 makeAvatar(url,nickname,className='chfc-avatar'){ 109 const avatar=document.createElement('div');avatar.className=className;avatar.textContent=(nickname||'?').slice(0,1).toUpperCase(); 110 if(url){const img=document.createElement('img');img.src=url;img.alt='';img.loading='lazy';img.referrerPolicy='no-referrer';img.onerror=()=>img.remove();avatar.append(img);} 111 return avatar; 112 } 113 destroy(){ clearInterval(this.timer); clearTimeout(this.typingTimer); document.removeEventListener('click',this.outside); document.removeEventListener('pointerdown',this.unlockRingtone); document.removeEventListener('keydown',this.unlockRingtone); if(this.ringtoneAudio){this.ringtoneAudio.pause();this.ringtoneAudio=null;} this.setTyping(false); const d=this.root.querySelector('[data-invite-dialog]'); if(d.open)d.close(); } 114 updateChars(){this.root.querySelector('[data-char-count]').textContent=`${this.textarea.value.length}/${this.textarea.maxLength}`;} 115 handleKeydown(e){ 116 const menu=this.root.querySelector('[data-mention-menu]'); 117 if(!menu.hidden&&this.mentionMatches.length){ 118 if(e.key==='ArrowDown'){e.preventDefault();this.mentionIndex=(this.mentionIndex+1)%this.mentionMatches.length;this.renderMentionMenu();return;} 119 if(e.key==='ArrowUp'){e.preventDefault();this.mentionIndex=(this.mentionIndex-1+this.mentionMatches.length)%this.mentionMatches.length;this.renderMentionMenu();return;} 120 if(e.key==='Enter'||e.key==='Tab'){e.preventDefault();this.insertMention(this.mentionMatches[this.mentionIndex]);return;} 121 if(e.key==='Escape'){e.preventDefault();this.closeMentionMenu();return;} 122 } 123 if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();this.form.requestSubmit();} 124 if(e.key==='Escape')this.closeEmoji(); 125 } 126 handleTyping(){ 127 this.setTyping(true); clearTimeout(this.typingTimer); this.typingTimer=setTimeout(()=>this.setTyping(false),2200); 128 } 129 async setTyping(active){
130 if(this.typingSent===active)return; this.typingSent=active; 131 try{await this.api.call('/typing',{method:'POST',body:JSON.stringify({active,with:this.current.id||''})});}catch(_){} 132 } 133 renderTyping(items){ 134 const names=(items||[]).map(i=>i.nickname).filter(Boolean); 135 if(!names.length){this.typingEl.textContent='';return;} 136 this.typingEl.textContent=names.length===1?`${names[0]} schreibt gerade â¦`:`${names.slice(0,2).join(' und ')} schreiben gerade â¦`; 137 } 138 139 emojiData(){return { 140 'Zuletzt':[], 141 'Smileys':[['ð','grinsen fröhlich'],['ð','lächeln froh'],['ð','freude lachen'],['ð','grinsen'],['ð','lachen'],['ð ','erleichtert'],['ð','tränen lachen'],['ð¤£','laut lachen'],['ð','lächeln'],['ð','freundlich'],['ð','umgedreht'],['ð','zwinkern'],['ð','verliebt'],['ð¥°','liebe'],['ð','kuss'],['ð','cool'],['ð¤','nerd'],['ð¤','denken'],['ð´','schlafen'],['ð¢','traurig'],['ð','weinen'],['ð¡','wütend'],['ð¤¯','wow'],['ð¥³','party']], 142 'Gesten':[['ð','daumen hoch ja'],['ð','daumen runter nein'],['ð','okay'],['âï¸','peace sieg'],['ð¤','glück'],['ð','applaus'],['ð','jubel'],['ð¤','handschlag'],['ð','bitte danke'],['ðª','stark'],['ð','winken hallo'],['ð¤','umarmung'],['ð¤¦','facepalm'],['ð¤·','keine ahnung']], 143 'Herzen':[['â¤ï¸','liebe herz rot'],['ð§¡','herz orange'],['ð','herz gelb'],['ð','herz grün'],['ð','herz blau'],['ð','herz lila'],['ð¤','herz schwarz'],['ð¤','herz weiÃ'],['ð','gebrochenes herz'],['ð','zwei herzen'],['ð¯','hundert']], 144 'Tiere':[['ð¶','hund'],['ð±','katze'],['ð','maus'],['ð°','hase'],['ð¦','fuchs'],['ð»','bär'],['ð¼','panda'],['ð¨','koala'],['ð¯','tiger'],['ð¦','löwe'],['ð¸','frosch'],['ðµ','affe']], 145 'Essen':[['ð','pizza'],['ð','burger'],['ð','pommes'],['ð','hotdog'],['ð¿','popcorn'],['ð¥','salat'],['ð','apfel'],['ð','erdbeere'],['ð°','kuchen'],['â','kaffee'],['ðº','bier'],['ð¥¤','getränk']], 146 'Aktivitäten':[['â½','fuÃball sport'],['ð','basketball'],['ð®','gaming spiel'],['ð¯','ziel'],['ð¸','gitarre musik'],['ð¬','film'],['ð','auto'],['âï¸','flugzeug reisen'],['ð','rakete'],['ð','pokal'],['ð','party'],['ð','geschenk']], 147 'Symbole':[['â ','ja erledigt'],['â','nein falsch'],['â ï¸','warnung'],['â¨','funkeln'],['â','stern'],['ð¥','feuer'],['ð','sonne'],['ð','mond'],['ð','glück'],['ð¬','chat'],['â','frage'],['â','ausrufezeichen']] 148 };} 149 buildEmoji(){ 150 const cats=this.root.querySelector('[data-emoji-categories]'); cats.textContent=''; 151 Object.keys(this.emojiData()).forEach((name,i)=>{const b=document.createElement('button');b.type='button';b.textContent=name;b.dataset.category=name;if(i===1)b.classList.add('is-active');b.onclick=()=>this.renderEmoji(name);cats.append(b);}); 152 this.renderEmoji(this.recentEmoji.length?'Zuletzt':'Smileys'); 153 } 154 emojiItems(category){ 155 if(category==='Zuletzt')return this.recentEmoji.map(e=>[e,'zuletzt']); 156 return this.emojiData()[category]||[]; 157 } 158 renderEmoji(category,filter=''){ 159 const grid=this.root.querySelector('[data-emoji-grid]');grid.textContent=''; 160 $$('.chfc-emoji-categories button',this.root).forEach(b=>b.classList.toggle('is-active',b.dataset.category===category)); 161 let all=filter?Object.entries(this.emojiData()).flatMap(([cat,items])=>cat==='Zuletzt'?[]:items):this.emojiItems(category); 162 const q=filter.toLocaleLowerCase(); if(q)all=all.filter(([,keywords])=>keywords.toLocaleLowerCase().includes(q)); 163 if(!all.length){const empty=document.createElement('p');empty.className='chfc-emoji-empty';empty.textContent='Keine Emojis gefunden.';grid.append(empty);return;} 164 all.forEach(([emoji])=>{const b=document.createElement('button');b.type='button';b.className='chfc-emoji-option';b.textContent=emoji;b.title=emoji;b.onclick=()=>this.insertEmoji(emoji);grid.append(b);}); 165 } 166 filterEmoji(value){const v=value.trim();if(v){this.renderEmoji('',v);}else{const active=this.root.querySelector('.chfc-emoji-categories .is-active');this.renderEmoji(active?.dataset.category||'Smileys');}} 167 toggleEmoji(e){e.stopPropagation();const p=this.root.querySelector('[data-emoji-picker]');p.hidden=!p.hidden;this.root.querySelector('[data-action="emoji"]').setAttribute('aria-expanded',String(!p.hidden));this.closeMentionMenu();if(!p.hidden)this.root.querySelector('[data-emoji-search]').focus();} 168 closeEmoji(){const p=this.root.querySelector('[data-emoji-picker]');p.hidden=true;this.root.querySelector('[data-action="emoji"]').setAttribute('aria-expanded','false');} 169 insertEmoji(emoji){const s=this.textarea.selectionStart,e=this.textarea.selectionEnd;this.textarea.setRangeText(emoji,s,e,'end');this.textarea.focus();this.recentEmoji=[emoji,...this.recentEmoji.filter(x=>x!==emoji)].slice(0,24);localStorage.setItem(RECENT_EMOJI_KEY,JSON.stringify(this.recentEmoji));this.updateChars();} 170 171 updateMentionMenu(){ 172 const pos=this.textarea.selectionStart, before=this.textarea.value.slice(0,pos), match=before.match(/(?:^|\s)@([^\s@]{0,24})$/u); 173 if(!match){this.closeMentionMenu();return;} 174 const q=(match[1]||'').toLocaleLowerCase(); 175 this.mentionMatches=this.userItems.filter(u=>u.public_id!==this.session.session_id&&u.nickname.toLocaleLowerCase().includes(q)).slice(0,8); 176 this.mentionIndex=0; this.renderMentionMenu(); 177 } 178 renderMentionMenu(){ 179 const menu=this.root.querySelector('[data-mention-menu]');menu.textContent=''; 180 if(!this.mentionMatches.length){menu.hidden=true;return;}
181 this.mentionMatches.forEach((u,i)=>{const b=document.createElement('button');b.type='button';b.className=i===this.mentionIndex?'is-active':'';b.innerHTML='<span class="chfc-mini-avatar"></span><span></span>';b.children[0].textContent=u.nickname.slice(0,1).toUpperCase();b.children[1].textContent=u.nickname;b.onmousedown=e=>{e.preventDefault();this.insertMention(u);};menu.append(b);});menu.hidden=false; 182 } 183 insertMention(user){ 184 const pos=this.textarea.selectionStart,before=this.textarea.value.slice(0,pos),match=before.match(/(?:^|\s)@([^\s@]{0,24})$/u);if(!match)return; 185 const start=pos-match[0].length+(match[0].startsWith(' ')?1:0);this.textarea.setRangeText(`@${user.nickname} `,start,pos,'end');this.closeMentionMenu();this.textarea.focus();this.updateChars(); 186 } 187 closeMentionMenu(){const m=this.root.querySelector('[data-mention-menu]');m.hidden=true;this.mentionMatches=[];} 188 189 updateSound(){$$('[data-action="sound"]',this.root).forEach(b=>{if(b.classList.contains('chfc-setting-toggle'))b.textContent=this.sound?'Ton an':'Ton aus';else b.innerHTML=this.sound?'ð <span>Ton an</span>':'ð <span>Ton aus</span>';b.setAttribute('aria-pressed',String(this.sound));b.classList.toggle('is-on',this.sound);});} 190 toggleSound(){this.sound=!this.sound;localStorage.setItem(SOUND_KEY,this.sound?'1':'0');this.updateSound();if(this.sound)this.beep(true);} 191 updateInviteSound(){$$('[data-action="invite-sound"]',this.root).forEach(b=>{b.textContent=this.inviteSound?'Ton an':'Ton aus';b.setAttribute('aria-pressed',String(this.inviteSound));b.classList.toggle('is-on',this.inviteSound);});} 192 toggleInviteSound(){this.inviteSound=!this.inviteSound;localStorage.setItem(INVITE_SOUND_KEY,this.inviteSound?'1':'0');this.updateInviteSound();if(this.inviteSound)this.beepInvite(true);} 193 updateBellReceive(){$$('[data-action="bell-receive"]',this.root).forEach(b=>{b.textContent=this.bellReceive?'Erlaubt':'Aus';b.setAttribute('aria-pressed',String(this.bellReceive));b.classList.toggle('is-on',this.bellReceive);});} 194 async toggleBellReceive(){const next=!this.bellReceive;try{const data=await this.api.call('/preferences',{method:'POST',body:JSON.stringify({bell_enabled:next})});this.bellReceive=Boolean(data.bell_enabled);localStorage.setItem(BELL_RECEIVE_KEY,this.bellReceive?'1':'0');this.updateBellReceive();this.renderUsers(this.userItems);this.updateRoomBell();this.toast(this.bellReceive?'Andere Nutzer können dich jetzt anklingeln.':'Anklingeln wurde deaktiviert.','success');}catch(err){this.toast(err.message,'error');}} 195 openInfoDialog(name){const d=this.root.querySelector(`[data-info-dialog="${name}"]`);if(!d)return;if(name==='settings'){this.updateSound();this.updateInviteSound();this.updateBellReceive();}typeof d.showModal==='function'?d.showModal():d.setAttribute('open','');} 196 beep(preview=false,mention=false){if(!this.sound)return;try{const AC=window.AudioContext||window.webkitAudioContext;if(!AC)return;if(!this.audio)this.audio=new AC();const n=this.audio.currentTime,o=this.audio.createOscillator(),g=this.audio.createGain();o.frequency.value=mention?1100:(preview?660:880);g.gain.setValueAtTime(.0001,n);g.gain.exponentialRampToValueAtTime(.09,n+.01);g.gain.exponentialRampToValueAtTime(.0001,n+.14);o.connect(g);g.connect(this.audio.destination);o.start(n);o.stop(n+.15);}catch(_){} } 197 beepInvite(preview=false){ 198 if(!this.inviteSound)return; 199 try{ 200 const AC=window.AudioContext||window.webkitAudioContext; 201 if(!AC)return; 202 if(!this.audio)this.audio=new AC(); 203 if(this.audio.state==='suspended')this.audio.resume().catch(()=>{}); 204 const n=this.audio.currentTime,master=this.audio.createGain(); 205 master.gain.setValueAtTime(.0001,n); 206 master.gain.exponentialRampToValueAtTime(preview?.11:.16,n+.015); 207 master.gain.setValueAtTime(preview?.11:.16,n+.42); 208 master.gain.exponentialRampToValueAtTime(.0001,n+.62); 209 master.connect(this.audio.destination);
210 [[520,0,.12],[720,.16,.12],[1040,.32,.28]].forEach(([frequency,offset,duration],index)=>{ 211 const o=this.audio.createOscillator(),voice=this.audio.createGain(); 212 o.type=index===2?'square':'triangle'; 213 o.frequency.setValueAtTime(frequency,n+offset); 214 if(index===2)o.frequency.exponentialRampToValueAtTime(1240,n+offset+duration); 215 voice.gain.setValueAtTime(.0001,n+offset); 216 voice.gain.exponentialRampToValueAtTime(index===2?.75:1,n+offset+.012); 217 voice.gain.exponentialRampToValueAtTime(.0001,n+offset+duration); 218 o.connect(voice);voice.connect(master); 219 o.start(n+offset);o.stop(n+offset+duration+.02); 220 }); 221 }catch(_){} 222 } 223 toast(text,type='info'){const stack=this.root.querySelector('[data-toast-stack]'),el=document.createElement('div');el.className='chfc-toast is-'+type;el.textContent=text;stack.append(el);setTimeout(()=>el.remove(),4200);} 224 async tick(){ 225 if(this.busy)return;this.busy=true; 226 try{ 227 const after=this.current.id?(this.last.private[this.current.id]||0):this.last.public; 228 const data=await this.api.call(`/state?after=${after}&with=${encodeURIComponent(this.current.id)}`); 229 if(data.preferences&&typeof data.preferences.bell_enabled==='boolean'){this.bellReceive=data.preferences.bell_enabled;localStorage.setItem(BELL_RECEIVE_KEY,this.bellReceive?'1':'0');this.updateBellReceive();} 230 this.syncPrivateChats(data.private_chats||[]); this.renderUsers(data.users||[]); this.updateRoomBell(); this.updateClearAction(); this.renderTyping(data.typing||[]); this.renderClearMarker(data.cleared_at||''); this.renderMessages(data.messages||[]); this.processInvites(data.incoming_invites||[],data.accepted_invites||[]); this.processBells(data.bells||[]); 231 this.root.querySelector('[data-status]').textContent=`Angemeldet als ${this.session.nickname}`;this.error.textContent='';this.initial=false; 232 }catch(err){this.error.textContent=err.message;if(/ungültig|abgelaufen/i.test(err.message)){this.api.save(null);this.destroy();this.onLogout('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');}} 233 finally{this.busy=false;} 234 } 235 syncPrivateChats(items){const next=new Map();items.forEach(i=>{if(i.public_id&&i.nickname)next.set(i.public_id,i.nickname);});this.privateChats=next;if(this.current.id&&!this.privateChats.has(this.current.id))this.openPublic();else this.renderChatTabs();} 236 renderChatTabs(){ 237 this.tabs.textContent=''; 238 const make=(id,name,icon)=>{ 239 const b=document.createElement('button');b.type='button';b.className='chfc-chat-tab'+(this.current.id===id?' is-active':''); 240 b.innerHTML=`<span>${icon}</span><span class="chfc-chat-tab-name"></span><span class="chfc-badge" hidden>0</span>`; 241 b.querySelector('.chfc-chat-tab-name').textContent=name; 242 const n=this.unread.get(id)||0,badge=b.querySelector('.chfc-badge');if(n){badge.hidden=false;badge.textContent=n;} 243 b.onclick=()=>id?this.openPrivate(id,name):this.openPublic(); 244 if(id){const close=document.createElement('span');close.className='chfc-chat-tab-close';close.textContent='Ã';close.title='Privatchat beenden';close.setAttribute('role','button');close.setAttribute('aria-label',`Privatchat mit ${name} beenden`);close.tabIndex=0;const end=e=>{e.preventDefault();e.stopPropagation();this.closePrivate(id,name);};close.onclick=end;close.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){end(e);}};b.append(close);} 245 this.tabs.append(b); 246 }; 247 make('','Ãffentlich','#');[...this.privateChats].sort((a,b)=>a[1].localeCompare(b[1],'de')).forEach(([id,name])=>make(id,name,'ð')); 248 } 249 async closePrivate(id,name){ 250 try{await this.api.call('/private/close',{method:'POST',body:JSON.stringify({with:id})});this.privateChats.delete(id);this.unread.delete(id);if(this.current.id===id)this.openPublic();else this.renderChatTabs();this.toast(`Privatchat mit ${name} beendet.`,'success');} 251 catch(err){this.toast(err.message,'error');} 252 }
253 renderPrivateList(){} 254 toggleFavorite(id){this.favorites.has(id)?this.favorites.delete(id):this.favorites.add(id);localStorage.setItem(FAVORITES_KEY,JSON.stringify([...this.favorites]));this.renderUsers(this.userItems);} 255 renderUsers(items){ 256 this.userItems=items;this.users.textContent='';this.root.querySelector('[data-user-count]').textContent=items.length; 257 const filtered=items.filter(u=>!this.searchTerm||u.nickname.toLocaleLowerCase().includes(this.searchTerm)).sort((a,b)=>{ 258 const ao=a.public_id===this.session.session_id,bo=b.public_id===this.session.session_id;if(ao!==bo)return ao?-1:1; 259 const af=this.favorites.has(a.public_id),bf=this.favorites.has(b.public_id);if(af!==bf)return af?-1:1;return a.nickname.localeCompare(b.nickname,'de'); 260 }); 261 filtered.forEach(u=>{const own=u.public_id===this.session.session_id,row=document.createElement('div');row.className='chfc-user'+(own?' is-own':'')+(this.favorites.has(u.public_id)?' is-favorite':''); 262 const main=document.createElement('div');main.className='chfc-user-main';main.innerHTML='<span class="chfc-status-dot"></span><span class="chfc-user-avatar"></span><span class="chfc-user-name"></span><small></small>';const ua=main.querySelector('.chfc-user-avatar');ua.textContent=(u.nickname||'?').slice(0,1).toUpperCase();if(u.avatar_url){const ui=document.createElement('img');ui.src=u.avatar_url;ui.alt='';ui.loading='lazy';ui.referrerPolicy='no-referrer';ui.onerror=()=>ui.remove();ua.append(ui);}main.querySelector('.chfc-user-name').textContent=u.nickname;const presence=main.querySelector('small');if(own){presence.textContent='du';presence.title='Du';presence.setAttribute('aria-label','Du');}else if(this.privateChats.has(u.public_id)){presence.textContent='ð';presence.title='Privatchat aktiv';presence.setAttribute('aria-label','Privatchat aktiv');}else{presence.textContent='â';presence.classList.add('chfc-online-icon');presence.title='Online';presence.setAttribute('aria-label','Online');} 263 row.append(main); 264 if(!own){const actions=document.createElement('span');actions.className='chfc-user-actions';if(Number(u.bell_enabled)!==0){const bell=document.createElement('button');bell.type='button';bell.className='chfc-user-bell-button';bell.textContent='ð';bell.title=`Bei ${u.nickname} klingeln`;bell.setAttribute('aria-label',bell.title);bell.onclick=()=>this.ringUser(u.public_id,u.nickname,bell);actions.append(bell);}if(this.config.privateChat){const priv=document.createElement('button');priv.type='button';priv.className='chfc-private-button';const active=this.privateChats.has(u.public_id);priv.textContent=active?'ð':'ð¬';priv.title=active?`Privatchat mit ${u.nickname} öffnen`:`Privatchat mit ${u.nickname} anfragen`;priv.setAttribute('aria-label',priv.title);priv.onclick=()=>active?this.openPrivate(u.public_id,u.nickname):this.requestPrivate(u.public_id,u.nickname);actions.append(priv);}const fav=document.createElement('button');fav.type='button';fav.className='chfc-favorite-button';fav.textContent=this.favorites.has(u.public_id)?'â ':'â';fav.title=this.favorites.has(u.public_id)?'Aus Favoriten entfernen':'Zu Favoriten hinzufügen';fav.onclick=()=>this.toggleFavorite(u.public_id);actions.append(fav);row.append(actions);}this.users.append(row); 265 }); 266 if(!filtered.length){const p=document.createElement('p');p.className='chfc-empty-private';p.textContent='Keine Benutzer gefunden.';this.users.append(p);} 267 } 268 applyMessageFilter(){ 269 const q=this.messageFilter;let visible=0,total=0; 270 $$('.chfc-message',this.messages).forEach(row=>{total++;const match=!q||(row.dataset.searchText||'').includes(q);row.hidden=!match;if(match)visible++;}); 271 this.messages.classList.toggle('is-filtering',Boolean(q)); 272 this.messages.setAttribute('aria-label',q?`${visible} von ${total} Nachrichten entsprechen dem Filter`:'Chatnachrichten'); 273 } 274 resetMessageFilter(){this.messageFilter='';this.messageFilterInput.value='';this.resetFilterButton.disabled=true;this.applyMessageFilter();this.messageFilterInput.focus();} 275 isNearBottom(){return this.messages.scrollHeight-this.messages.scrollTop-this.messages.clientHeight<110;} 276 clearNewMessageNotice(){this.pendingNew=0;this.newMessagesButton.hidden=true;this.newMessagesButton.textContent='â Neue Nachrichten';} 277 showNewMessageNotice(count){this.pendingNew+=count;this.newMessagesButton.textContent=`â ${this.pendingNew} neue Nachricht${this.pendingNew===1?'':'en'}`;this.newMessagesButton.hidden=false;} 278 jumpToLatest(){this.messages.scrollTo({top:this.messages.scrollHeight,behavior:'smooth'});this.clearNewMessageNotice();} 279 280 appendHighlightedText(container,text){ 281 const nick=this.session.nickname.replace(/[.*+?^${}
281()|[\]\\]/g,'\\$&'),re=new RegExp(`(@${nick})(?=\\s|[.,!?;:]|$)`,'giu');let last=0,match; 282 while((match=re.exec(text))){container.append(document.createTextNode(text.slice(last,match.index)));const mark=document.createElement('mark');mark.className='chfc-mention';mark.textContent=match[0];container.append(mark);last=re.lastIndex;}container.append(document.createTextNode(text.slice(last))); 283 } 284 updateClearAction(){ 285 const button=this.root.querySelector('[data-action="clear-for-me"]'); 286 if(!button)return; 287 const allowed=this.current.id?Boolean(this.config.userClearPrivate):Boolean(this.config.userClearPublic); 288 button.hidden=!allowed; 289 const label=button.querySelector('[data-clear-label]'); 290 if(label)label.textContent=this.current.id?'Privatchat für mich leeren':'Ãffentlichen Chat für mich leeren'; 291 } 292 renderClearMarker(value){ 293 const existing=this.messages.querySelector('[data-clear-marker]'); 294 if(!value){existing?.remove();return;} 295 if(existing&&existing.dataset.value===value)return; 296 existing?.remove(); 297 const marker=document.createElement('div'); 298 marker.className='chfc-clear-marker';marker.dataset.clearMarker='1';marker.dataset.value=value; 299 const date=new Date(String(value).replace(' ','T')+'Z'); 300 marker.textContent=`Du hast diesen Chatverlauf am ${date.toLocaleDateString()} um ${date.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})} für dich geleert.`; 301 this.messages.prepend(marker); 302 } 303 async clearForMe(){ 304 const privateRoom=Boolean(this.current.id); 305 const title=privateRoom?'Diesen Privatchat':'Den öffentlichen Chat'; 306 if(!window.confirm(`${title} nur für dich leeren? 307 308Andere Teilnehmer behalten ihren Verlauf. Die Nachrichten werden nicht vom Server gelöscht.`))return; 309 const button=this.root.querySelector('[data-action="clear-for-me"]'); 310 if(button)button.disabled=true; 311 try{ 312 const data=await this.api.call('/history/clear',{method:'POST',body:JSON.stringify({with:this.current.id||''})}); 313 if(privateRoom)this.last.private[this.current.id]=0;else this.last.public=0; 314 this.messages.textContent='';this.lastRendered=null;this.renderedMessageIds.clear();this.clearNewMessageNotice(); 315 this.renderClearMarker(data.cleared_at||''); 316 this.toast('Der Chatverlauf wurde nur für dich geleert.','success'); 317 await this.tick(); 318 }catch(err){this.toast(err.message,'error');} 319 finally{if(button)button.disabled=false;} 320 } 321 renderMessages(items){ 322 const nearBottom=this.isNearBottom();let incoming=false,mentioned=false; 323 items.forEach(m=>{ 324 const messageId=Number(m.id)||0; 325 if(messageId&&this.renderedMessageIds.has(messageId))return; 326 if(messageId)this.renderedMessageIds.add(messageId); 327 const own=m.sender_public_id===this.session.session_id;if(!own)incoming=true; 328 const isMention=!own&&new RegExp(`@${this.session.nickname.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}(?=\\s|[.,!?;:]|$)`,'iu').test(m.message||'');if(isMention)mentioned=true; 329 const timeValue=new Date(String(m.created_at).replace(' ','T')+'Z'); 330 const grouped=this.lastRendered&&this.lastRendered.sender===m.sender_public_id&&(timeValue-this.lastRendered.time)<300000; 331 const row=document.createElement('article');row.className='chfc-message'+(own?' is-own':'')+(isMention?' is-mentioned':'')+(grouped?' is-grouped':'');row.dataset.searchText=`${m.sender_nickname||''} ${m.message||''} ${m.attachment_name||''}`.toLocaleLowerCase(); 332 const avatar=this.makeAvatar(m.sender_avatar_url||'',m.sender_nickname||'?'); 333 const bubble=document.createElement('div');bubble.className='chfc-bubble';const meta=document.createElement('div');meta.className='chfc-meta';const name=document.createElement('strong');name.textContent=own?'Du':m.sender_nickname;const time=document.createElement('time');time.textContent=timeValue.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});meta.append(name,time);bubble.append(meta); 334 if(m.attachment_url){const link=document.createElement('a');link.className='chfc-image-link';link.href=m.attachment_url;link.target='_blank';link.rel='noopener noreferrer';const img=document.createElement('img');img.className='chfc-chat-image';img.src=m.attachment_url;img.alt=m.attachment_name||'Geteiltes Bild';img.loading='lazy';link.append(img);bubble.append(link);} 335 if(m.message){const body=document.createElement('div');body.className='chfc-text';this.appendHighlightedText(body,m.message);bubble.append(body);} 336 row.append(avatar,bubble);this.messages.append(row);this.lastRendered={sender:m.sender_public_id,time:timeValue}; 337 if(this.current.id)this.last.private[this.current.id]=Math.max(this.last.private[this.current.id]||0,Number(m.id));else this.last.public=Math.max(this.last.public,Number(m.id)); 338 }); 339 if(items.length)this.applyMessageFilter(); 340 if(items.length&&(nearBottom||this.initial)){this.messages.scrollTop=this.messages.scrollHeight;this.clearNewMessageNotice();} 341 else if(items.length)this.showNewMessageNotice(items.length); 342 if(incoming&&!this.initial){this.beep(false,mentioned);document.title=mentioned?`@ Erwähnung â ${this.config.brandText||'CH WP Chat'}`:`â Neue Nachricht â ${this.config.brandText||'CH WP Chat'}`;setTimeout(()=>document.title=this.config.brandText||'CH WP Chat',2500);} 343 } 344 handleClipboardPaste(event){ 345 const clipboard=event.clipboardData;if(!clipboard)return; 346 let file=null; 347 if(clipboard.items){for(const item of clipboard.items){if(item.kind==='file'&&/^image\//i.test(item.type||'')){file=item.getAsFile();break;}}} 348 if(!file&&clipboard.files){file=[...clipboard.files].find(f=>/^image\//i.test(f.type||''))||null;} 349 if(!file)return; 350 event.preventDefault(); 351 const extension=({ 'image/jpeg':'jpg','image/png':'png','image/gif':'gif','image/webp':'webp' })[file.type]||'png'; 352 if(!file.name||file.name==='image.png')file=new File([file],`screenshot-${new Date().toISOString().replace(/[:.]/g,'-')}.${extension}`,{type:file.type||'image/png'}); 353 this.uploadImageFile(file,null,true); 354 } 355 async uploadImage(input){ 356 const file=input.files&&input.files[0];if(!file)return; 357 await this.uploadImageFile(file,input,false); 358 } 359 async uploadImageFile(file,input=null,fromClipboard=false){ 360 if(!/^image\/(jpeg|png|gif|webp)$/i.test(file.type)){this.toast('Erlaubt sind JPG, PNG, GIF und WEBP.','error');if(input)input.value='';return;} 361 const max=Math.max(1,Number(this.config.maxImageMb||5))*1024*1024;if(file.size>max){this.toast(`Das Bild darf höchstens ${this.config.maxImageMb||5} MB groà sein.`,'error');if(input)input.value='';return;} 362 const data=new FormData();data.append('image',file,file.name||'screenshot.png');data.append('receiver_id',this.current.id||'');data.append('caption',this.textarea.value.trim());
363 const choose=this.root.querySelector('[data-action="choose-image"]');if(choose)choose.disabled=true;this.textarea.disabled=true; 364 try{await this.api.call('/upload',{method:'POST',body:data});this.textarea.value='';this.updateChars();await this.tick();this.toast(fromClipboard?'Screenshot aus der Zwischenablage gesendet.':'Bild gesendet.','success');} 365 catch(err){this.toast(err.message,'error');}finally{if(choose)choose.disabled=false;this.textarea.disabled=false;if(input)input.value='';this.textarea.focus();} 366 } 367 async ringUser(id,name,button=null){ 368 if(!id)return; 369 if(button)button.disabled=true; 370 try{ 371 const data=await this.api.call('/bell',{method:'POST',body:JSON.stringify({receiver_id:id})}); 372 this.toast(`Bei ${data.receiver_nickname||name} wurde geklingelt.`,'success'); 373 }catch(err){this.toast(err.message,'error');} 374 finally{if(button)setTimeout(()=>{button.disabled=false;},800);} 375 } 376 ringCurrentUser(){if(this.current.id)this.ringUser(this.current.id,this.current.name,this.root.querySelector('[data-action="ring-user"]'));} 377 updateRoomBell(){const bell=this.root.querySelector('[data-action="ring-user"]');if(!bell)return;if(!this.current.id){bell.hidden=true;return;}const user=this.userItems.find(u=>u.public_id===this.current.id);const enabled=!user||Number(user.bell_enabled)!==0;bell.hidden=!enabled;if(enabled){bell.title=`Bei ${this.current.name} klingeln`;bell.setAttribute('aria-label',bell.title);}} 378 processBells(items){ 379 if(!this.bellReceive)return; 380 items.forEach(item=>{ 381 this.beepBell(); 382 this.toast(`${item.sender_nickname||'Ein Benutzer'} klingelt bei dir. ð`,'success'); 383 }); 384 } 385 customRingtone(){ 386 const url=String(this.config.ringtoneUrl||'').trim(); 387 if(!url)return null; 388 if(!this.ringtoneAudio||this.ringtoneAudio.src!==new URL(url,location.href).href){ 389 this.ringtoneAudio=new Audio(url); 390 this.ringtoneAudio.preload='auto'; 391 this.ringtoneAudio.volume=1; 392 } 393 return this.ringtoneAudio; 394 } 395 unlockCustomRingtone(){ 396 const audio=this.customRingtone(); 397 if(!audio||this.ringtoneUnlocked)return; 398 const oldMuted=audio.muted; audio.muted=true; 399 const promise=audio.play(); 400 if(promise&&promise.then){promise.then(()=>{audio.pause();audio.currentTime=0;audio.muted=oldMuted;this.ringtoneUnlocked=true;}).catch(()=>{audio.muted=oldMuted;});} 401 } 402 beepBell(){ 403 const custom=this.customRingtone(); 404 if(custom){ 405 try{ 406 custom.pause();custom.currentTime=0;custom.muted=false; 407 const play=custom.play(); 408 if(play&&play.catch)play.catch(()=>this.beepBellFallback()); 409 return; 410 }catch(_){this.beepBellFallback();return;} 411 } 412 this.beepBellFallback(); 413 } 414 beepBellFallback(){
415 try{ 416 const AC=window.AudioContext||window.webkitAudioContext;if(!AC)return; 417 if(!this.audio)this.audio=new AC(); 418 if(this.audio.state==='suspended')this.audio.resume().catch(()=>{}); 419 420 const start=this.audio.currentTime+.03; 421 const ringDuration=.62; 422 const ringGap=.34; 423 const ringsPerSequence=3; 424 const sequencePause=.85; 425 const sequenceDuration=(ringsPerSequence-1)*(ringDuration+ringGap)+ringDuration; 426 const sequenceCount=4; 427 const totalDuration=sequenceCount*sequenceDuration+(sequenceCount-1)*sequencePause; 428 429 const master=this.audio.createGain(); 430 master.gain.setValueAtTime(.0001,start); 431 master.gain.linearRampToValueAtTime(.24,start+.025); 432 master.gain.setValueAtTime(.24,start+totalDuration-.18); 433 master.gain.exponentialRampToValueAtTime(.0001,start+totalDuration); 434 master.connect(this.audio.destination); 435 436 for(let sequence=0;sequence<sequenceCount;sequence++){ 437 const sequenceStart=start+sequence*(sequenceDuration+sequencePause); 438 for(let repeat=0;repeat<ringsPerSequence;repeat++){ 439 const ringStart=sequenceStart+repeat*(ringDuration+ringGap); 440 const ringGain=this.audio.createGain(); 441 ringGain.gain.setValueAtTime(.0001,ringStart); 442 ringGain.gain.linearRampToValueAtTime(1,ringStart+.018); 443 ringGain.gain.setValueAtTime(1,ringStart+ringDuration-.07); 444 ringGain.gain.exponentialRampToValueAtTime(.0001,ringStart+ringDuration); 445 ringGain.connect(master); 446 447 const tremolo=this.audio.createOscillator(); 448 const tremoloDepth=this.audio.createGain(); 449 tremolo.type='sine'; 450 tremolo.frequency.setValueAtTime(20,ringStart); 451 tremoloDepth.gain.setValueAtTime(.38,ringStart); 452 tremolo.connect(tremoloDepth); 453 tremoloDepth.connect(ringGain.gain); 454 tremolo.start(ringStart); 455 tremolo.stop(ringStart+ringDuration+.02); 456 457 [440,480].forEach((frequency,index)=>{ 458 const oscillator=this.audio.createOscillator(); 459 const voice=this.audio.createGain(); 460 oscillator.type=index===0?'triangle':'sine'; 461 oscillator.frequency.setValueAtTime(frequency,ringStart); 462 voice.gain.setValueAtTime(index===0?.58:.46,ringStart); 463 oscillator.connect(voice);voice.connect(ringGain); 464 oscillator.start(ringStart);oscillator.stop(ringStart+ringDuration+.02); 465 }); 466 467 const overtone=this.audio.createOscillator(); 468 const overtoneGain=this.audio.createGain(); 469 overtone.type='square'; 470 overtone.frequency.setValueAtTime(960,ringStart); 471 overtoneGain.gain.setValueAtTime(.10,ringStart); 472 overtoneGain.gain.exponentialRampToValueAtTime(.0001,ringStart+ringDuration*.8); 473 overtone.connect(overtoneGain);overtoneGain.connect(ringGain); 474 overtone.start(ringStart);overtone.stop(ringStart+ringDuration+.02); 475 } 476 } 477 }catch(_){} 478 } 479 async requestPrivate(id,name){try{await this.api.call('/invite',{method:'POST',body:JSON.stringify({receiver_id:id})});this.toast(`Anfrage an ${name} gesendet.`,'success');}catch(err){this.toast(err.message,'error');}} 480 processInvites(incoming,accepted){if(!this.currentInvite){const next=incoming.find(i=>!this.seenInvites.has(Number(i.id)));if(next){this.seenInvites.add(Number(next.id));this.currentInvite=next;this.showInvite(next);this.beepInvite();}}accepted.forEach(i=>{const id=Number(i.id);if(this.acceptedSeen.has(id))return;this.acceptedSeen.add(id);this.privateChats.set(i.receiver_public_id,i.receiver_nickname||'Privatchat');this.renderPrivateList();this.toast(`${i.receiver_nickname||'Der Nutzer'} hat deine Einladung angenommen.`,'success');this.openPrivate(i.receiver_public_id,i.receiver_nickname||'Privatchat');});} 481 showInvite(i){const d=this.root.querySelector('[data-invite-dialog]');this.root.querySelector('[data-invite-name]').textContent=i.sender_nickname;typeof d.showModal==='function'?d.showModal():d.setAttribute('open','');} 482 async respondInvite(decision){if(!this.currentInvite)return;const i=this.currentInvite,d=this.root.querySelector('[data-invite-dialog]');
482$$('button',d).forEach(b=>b.disabled=true);try{const data=await this.api.call('/invite/respond',{method:'POST',body:JSON.stringify({invite_id:i.id,decision})});d.close?.();this.currentInvite=null;if(decision==='accepted'){this.privateChats.set(data.sender_id,data.sender_nickname);this.openPrivate(data.sender_id,data.sender_nickname);this.toast('Privatchat geöffnet.','success');}else this.toast('Einladung abgelehnt.');}catch(err){this.toast(err.message,'error');}finally{$$('button',d).forEach(b=>b.disabled=false);}} 483 async send(e){ 484 e.preventDefault(); 485 if(this.sending)return; 486 const message=this.textarea.value.trim(); 487 if(!message)return; 488 this.sending=true; 489 const submit=this.form.querySelector('[type="submit"]'); 490 submit.disabled=true; 491 this.setTyping(false); 492 const clientMessageId=(window.crypto&&typeof window.crypto.randomUUID==='function')?window.crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).slice(2)}`; 493 try{ 494 await this.api.call('/messages',{method:'POST',body:JSON.stringify({message,receiver_id:this.current.id||'',client_message_id:clientMessageId})}); 495 if(this.textarea.value.trim()===message)this.textarea.value=''; 496 this.updateChars();this.closeMentionMenu();await this.tick();this.textarea.focus(); 497 }catch(err){this.toast(err.message,'error');} 498 finally{this.sending=false;submit.disabled=false;} 499 } 500 openPrivate(id,name){this.setTyping(false);this.resetMessageFilter();this.current={id,name};this.messages.textContent='';this.lastRendered=null;this.renderedMessageIds.clear();this.clearNewMessageNotice();this.unread.set(id,0);this.root.querySelector('[data-room-title]').textContent=name;this.updateRoomBell();this.root.querySelector('[data-room-subtitle]').textContent='Privates Gespräch â nur für euch beide sichtbar.';this.updateClearAction();this.root.querySelector('[data-action="public"]').classList.remove('is-active');this.renderPrivateList();this.renderChatTabs();this.tick();} 501 openPublic(){this.setTyping(false);this.resetMessageFilter();this.current={id:'',name:'Ãffentlicher Chat'};this.messages.textContent='';this.lastRendered=null;this.renderedMessageIds.clear();this.clearNewMessageNotice();this.root.querySelector('[data-room-title]').textContent='Ãffentlicher Chat';const bell=this.root.querySelector('[data-action="ring-user"]');if(bell)bell.hidden=true;this.root.querySelector('[data-room-subtitle]').textContent='Nachrichten sind für alle anwesenden Nutzer sichtbar.';this.updateClearAction();this.root.querySelector('[data-action="public"]').classList.add('is-active');this.renderPrivateList();this.renderChatTabs();this.tick();} 502 async leave(){const b=this.root.querySelector('[data-action="leave"]');b.disabled=true;try{await this.api.call('/leave',{method:'POST',body:'{}'});}catch(_){}this.api.save(null);this.destroy();this.onLogout('Du wurdest abgemeldet.');} 503 } 504 505 const init=()=>$$('[data-chfc-window]:not([data-chfc-ready])').forEach(el=>{el.dataset.chfcReady='1';new App(el);}); 506 document.readyState==='loading'?document.addEventListener('DOMContentLoaded',init,{once:true}):init(); 507})(); 508/* Ende CH WP Chat by Conny Henn - https://blog.hennweb.de/ch-wp-chat/ */
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.