1/* ================================================================
2 Nexus Forum — app.js v2
3 ================================================================ */
4'use strict';
5
6
7function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
8
9
10
11function tryEmbed(url) {
12 var m;
13
14 m = url.match(/youtube\.com\/watch\?.*v=([a-zA-Z0-9_\-]{11})/);
15 if (m) return ytEmbed(m[1]);
16
17 m = url.match(/youtu\.be\/([a-zA-Z0-9_\-]{11})/);
18 if (m) return ytEmbed(m[1]);
19
20 m = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_\-]{11})/);
21 if (m) return ytEmbed(m[1], true);
22
23 m = url.match(/vimeo\.com\/(\d{5,12})/);
24 if (m) return embedIframe('https://player.vimeo.com/video/'+m[1]+'?dnt=1', 'Vimeo');
25
26 m = url.match(/open\.spotify\.com\/(track|album|playlist|episode|artist)\/([a-zA-Z0-9]+)/);
27 if (m) {
28 var h = (m[1]==='track'||m[1]==='episode') ? '152' : '352';
29 return '<div class="embed-spotify"><iframe src="https://open.spotify.com/embed/'+m[1]+'/'+m[2]+'" width="100%" height="'+h+'" frameborder="0" allow="autoplay;clipboard-write;encrypted-media;fullscreen" loading="lazy"></iframe></div>';
30 }
31
32 if (/soundcloud\.com\/[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_]+/.test(url)) {
33 return '<div class="embed-sc"><iframe width="100%" height="166" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url='+encodeURIComponent(url)+'&color=%233b82f6&auto_play=false"></iframe></div>';
34 }
35
36 m = url.match(/twitch\.tv\/videos\/(\d+)/);
37 if (m) return embedIframe('https://player.twitch.tv/?video=v'+m[1]+'&parent='+location.hostname+'&autoplay=false', 'Twitch VOD');
38 m = url.match(/twitch\.tv\/([a-zA-Z0-9_]{4,25})(?:\/|$|\?)/);
39 if (m) return embedIframe('https://player.twitch.tv/?channel='+m[1]+'&parent='+location.hostname+'&autoplay=false', 'Twitch');
40
41 m = url.match(/(?:twitter|x)\.com\/[a-zA-Z0-9_]+\/status\/(\d+)/);
42 if (m) return '<div class="embed-tweet" data-tweet-id="'+m[1]+'"><a href="'+esc(url)+'" target="_blank" rel="noopener" class="tweet-fallback">🐦 View on Twitter/X →</a></div>';
43
44 m = url.match(/streamable\.com\/([a-zA-Z0-9]+)/);
45 if (m) return embedIframe('https://streamable.com/e/'+m[1], 'Streamable');
46
47 m = url.match(/dailymotion\.com\/video\/([a-zA-Z0-9]+)/);
48 if (m) return embedIframe('https://www.dailymotion.com/embed/video/'+m[1], 'Dailymotion');
49
50 m = url.match(/loom\.com\/share\/([a-zA-Z0-9]+)/);
51 if (m) return embedIframe('https://www.loom.com/embed/'+m[1], 'Loom');
52
53 m = url.match(/codepen\.io\/([a-zA-Z0-9\-_]+)\/pen\/([a-zA-Z0-9]+)/);
54 if (m) return embedIframeTall('https://codepen.io/'+m[1]+'/embed/'+m[2]+'?default-tab=result', 'CodePen', 420);
55
56 m = url.match(/jsfiddle\.net\/([a-zA-Z0-9\/]+)/);
57 if (m) return embedIframeTall('https://jsfiddle.net/'+m[1].replace(/\/$/,'')+'/embedded/result', 'JSFiddle', 380);
58
59 m = url.match(/ted\.com\/talks\/([a-zA-Z0-9_]+)/);
60 if (m) return embedIframe('https://embed.ted.com/talks/'+m[1], 'TED Talk');
61 return null;
62}
63
64function ytEmbed(id, isShort) {
65 var pad = isShort ? 'padding-bottom:177.78%;max-width:360px' : 'padding-bottom:56.25%';
66 return '<div class="embed-wrap" style="'+pad+'"><iframe class="embed-yt" src="https://www.youtube-nocookie.com/embed/'+esc(id)+'?rel=0&modestbranding=1" allowfullscreen loading="lazy" title="YouTube"></iframe></div>';
67}
68function embedIframe(src, label) {
69 return '<div class="embed-wrap"><iframe class="embed-yt" src="'+esc(src)+'" allowfullscreen loading="lazy" title="'+esc(label||'')+'"></iframe></div>';
70}
71function embedIframeTall(src, label, h) {
72 return '<div class="embed-wrap" style="padding-bottom:0;height:'+(h||400)+'px"><iframe class="embed-yt" src="'+esc(src)+'" allowfullscreen loading="lazy" title="'+esc(label||'')+'"></iframe></div>';
73}
74function processEmbeds(html) {
75 return html.replace(/<p>\s*(https?:\/\/[^\s<>"']+)\s*<\/p>/gi, function(match, url) {
76 var em = tryEmbed(url);
77 return em !== null ? em : match;
78 });
79}
80
81
82
83function md(raw) {
84 if (!raw) return '';
85 var s = raw;
86 var fences = [], quotes = [], inlines = [];
87
88
89 var fenceLines = s.split('\n');
90 var fenceOut = [], inFence = false, fLang = '', fType = '', fBuf = [];
91
92 var buildFence = function(lang, lines) {
93 var code = lines.join('\n');
94 var ll = lang ? lang.toLowerCase() : '';
95 var copyBtn = '<button class="cb-copy" onclick="cbCopy(this)" title="Copy" aria-label="Copy code">'
96 + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '
97 + 'stroke-linecap="round" stroke-linejoin="round" width="13" height="13">'
98 + '<rect x="9" y="9" width="13" height="13" rx="2"/>'
99 + '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'
100 + '</svg><span>Copy</span></button>';
101 var hdr, blk;
102 if (ll) {
103 hdr = '<div class="cb-header"><span class="cb-lang">' + esc(ll) + '</span>' + copyBtn + '</div>';
104 blk = '<pre class="code-block" data-lang="' + esc(ll) + '">'
105 + '<code class="language-' + esc(ll) + '">' + esc(code) + '</code></pre>';
106 } else {
107 hdr = '<div class="cb-header cb-header-nolang">' + copyBtn + '</div>';
108 blk = '<pre class="code-block"><code>' + esc(code) + '</code></pre>';
109 }
110 var i = fences.length;
111 fences.push('<div class="code-block-wrap">' + hdr + blk + '</div>');
112 return '\x02FENCE' + i + 'FNCE\x03';
113 };
114
115 for (var fi = 0; fi < fenceLines.length; fi++) {
116 var fl = fenceLines[fi];
117 if (!inFence) {
118 var tm = fl.match(/^```([ \t]*\w*)[ \t]*$/);
119 if (tm) { inFence = true; fType = 'triple'; fLang = tm[1].trim(); fBuf = []; continue; }
120 if (/^`[ \t]*$/.test(fl)) { inFence = true; fType = 'single'; fLang = ''; fBuf = []; continue; }
121 fenceOut.push(fl);
122 } else {
123 var isClose = (fType === 'triple' && /^```[ \t]*$/.test(fl))
124 || (fType === 'single' && /^`[ \t]*$/.test(fl));
125 if (isClose) { fenceOut.push(buildFence(fLang, fBuf)); inFence = false; fBuf = []; }
126 else { fBuf.push(fl); }
127 }
128 }
129 if (inFence && fBuf.length) fenceOut.push(buildFence(fLang, fBuf));
130 s = fenceOut.join('\n');
131
132
133 var bqLines = s.split('\n'), bqOut = [], bqBuf = [];
134 var flushBq = function() {
135 if (!bqBuf.length) return;
136 var inner = bqBuf.join('\n');
137 inner = inner.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
138 inner = inner.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
139 inner = inner.replace(/\*([^\*\n]+)\*/g, '<em>$1</em>');
140 inner = inner.replace(/~~(.+?)~~/g, '<del>$1</del>');
141 var bqParts = inner.split('\n').filter(function(l){return l.trim();});
142 var content = bqParts.length > 1
143 ? bqParts.map(function(l){return '<p>'+l.trim()+'</p>';}).join('')
144 : inner.trim();
145 var i = quotes.length;
146 quotes.push('<blockquote class="post-quote">' + content + '</blockquote>');
147 bqOut.push('\x02BQUOT' + i + 'BQUT\x03');
148 bqBuf = [];
149 };
150 for (var bi = 0; bi < bqLines.length; bi++) {
151 var bm = bqLines[bi].match(/^(?:>|>) ?(.*)/);
152 if (bm) { bqBuf.push(bm[1]); }
153 else { flushBq(); bqOut.push(bqLines[bi]); }
154 }
155 flushBq();
156 s = bqOut.join('\n');
157
158
159 s = s.replace(/`([^`\n]+)`/g, function(_, code) {
160 var i = inlines.length;
161 inlines.push('<code class="inline-code">' + esc(code) + '</code>');
162 return '\x02INLIN' + i + 'INLN\x03';
163 });
164
165
166 s = s.replace(/^#{6} (.+)$/gm, '<h6>$1</h6>');
167 s = s.replace(/^#{5} (.+)$/gm, '<h5>$1</h5>');
168 s = s.replace(/^#{4} (.+)$/gm, '<h4>$1</h4>');
169 s = s.replace(/^#{3} (.+)$/gm, '<h3>$1</h3>');
170 s = s.replace(/^#{2} (.+)$/gm, '<h2>$1</h2>');
171 s = s.replace(/^# (.+)$/gm, '<h1>$1</h1>');
172 s = s.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '<hr>');
173
174 s = s.replace(/^[ \t]*[*\-+] (.+)$/gm, '<li>$1</li>');
175 s = s.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
176 s = s.replace(/<\/ul>\s*<ul>/g, '');
177
178 s = s.replace(/^[ \t]*\d+\. (.+)$/gm, '<oli>$1</oli>');
179 s = s.replace(/((?:<oli>.*<\/oli>\n?)+)/g, '<ol>$1</ol>');
180 s = s.replace(/<\/ol>\s*<ol>/g, '');
181 s = s.replace(/<oli>/g, '<li>').replace(/<\/oli>/g, '</li>');
182
183
184 s = s.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
185 s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
186 s = s.replace(/\*([^\*\n]+)\*/g, '<em>$1</em>');
187 s = s.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>');
188 s = s.replace(/__(.+?)__/g, '<strong>$1</strong>');
189 s = s.replace(/_([^_\n]+)_/g, '<em>$1</em>');
190 s = s.replace(/~~(.+?)~~/g, '<del>$1</del>');
191
192 s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(_,alt,src){
193 if (!/^https?:\/\/|^\
194 return '<img src="'+esc(src)+'" alt="'+esc(alt)+'" loading="lazy" onclick="lightbox(this)">';
195 });
196 s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_,txt,url){
197 if (!/^https?:\/\/|^\
198 return '<a href="'+esc(url)+'" target="_blank" rel="noopener noreferrer nofollow">'+esc(txt)+'</a>';
199 });
200
201
202 s = s.replace(/(^|[ \t]|<p>)(https?:\/\/[^\s<>"']+)/gm, function(_, pre, url) {
203 if (/^<(div|iframe|a|pre)/.test(url)) return pre + url;
204 var em = tryEmbed(url);
205 if (em) return pre + em;
206 return pre + '<a href="' + esc(url) + '" target="_blank" rel="noopener noreferrer nofollow">' + esc(url) + '</a>';
207 });
208
209
210 var pLines = s.split('\n'), pOut = [], para = '';
211 var blockRe = /^(<(h[1-6]|ul|ol|blockquote|pre|table|hr|img|div|figure|p)|\x02FENCE|\x02BQUOT)/;
212 function flush(){ if(para.trim()){ pOut.push('<p>'+para.trim()+'</p>'); para=''; } }
213 for (var pi = 0; pi < pLines.length; pi++) {
214 var pl = pLines[pi];
215 if (!pl.trim()) { flush(); }
216 else if (blockRe.test(pl.trim())) { flush(); pOut.push(pl); }
217 else { para += (para ? ' ' : '') + pl; }
218 }
219 flush();
220 s = pOut.join('\n');
221
222
223 fences.forEach(function(b,i){ s = s.replace('\x02FENCE' + i + 'FNCE\x03', b); });
224 quotes.forEach(function(b,i){ s = s.replace('\x02BQUOT' + i + 'BQUT\x03', b); });
225 inlines.forEach(function(b,i){ s = s.replace('\x02INLIN' + i + 'INLN\x03', b); });
226
227 return s;
228}
229
230
231
232function renderAllMd() {
233 document.querySelectorAll('.md[data-raw]').forEach(function(el) {
234 el.innerHTML = md(atob(el.getAttribute('data-raw')));
235 el.removeAttribute('data-raw');
236 });
237 loadTwitterWidgets();
238}
239
240
241function lightbox(img) {
242 var ov = document.createElement('div');
243 ov.className = 'lightbox';
244 var im = document.createElement('img');
245 im.src = img.src; im.alt = img.alt;
246 ov.appendChild(im);
247 ov.onclick = function(){ov.remove();};
248 document.body.appendChild(ov);
249}
250
251
252function timeAgo(dt) {
253 var diff = Math.floor((Date.now() - new Date(dt)) / 1000);
254 if (diff < 60) return 'just now';
255 if (diff < 3600) return Math.floor(diff/60) + 'm ago';
256 if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
257 if (diff < 604800) return Math.floor(diff/86400) + 'd ago';
258 return new Date(dt).toLocaleDateString();
259}
260function renderTimeAgo() {
261 document.querySelectorAll('.ago[data-ts]').forEach(function(el) {
262 el.textContent = timeAgo(el.dataset.ts);
263 el.title = new Date(el.dataset.ts).toLocaleString();
264 });
265}
266
267
268var burger = document.getElementById('burgerBtn');
269var sidebar = document.getElementById('sidebar');
270var sbOv = document.getElementById('sbOverlay');
271if (burger && sidebar) {
272 burger.addEventListener('click', function(){
273 var open = sidebar.classList.toggle('open');
274 burger.classList.toggle('open', open);
275 if (sbOv) sbOv.classList.toggle('show', open);
276 document.body.style.overflow = open ? 'hidden' : '';
277 });
278 if (sbOv) sbOv.addEventListener('click', function(){
279 sidebar.classList.remove('open');
280 burger.classList.remove('open');
281 sbOv.classList.remove('show');
282 document.body.style.overflow = '';
283 });
284}
285
286
287function setupDrop(btnId, dropId, onOpen) {
288 var btn = document.getElementById(btnId);
289 var drop = document.getElementById(dropId);
290 if (!btn || !drop) return;
291 btn.addEventListener('click', function(e) {
292 e.stopPropagation();
293 var opening = !drop.classList.contains('show');
294 document.querySelectorAll('.notif-drop.show,.user-drop.show').forEach(function(d){d.classList.remove('show');});
295 if (opening) { drop.classList.add('show'); if (onOpen) onOpen(); }
296 });
297}
298document.addEventListener('click', function(){
299 document.querySelectorAll('.notif-drop.show,.user-drop.show').forEach(function(d){d.classList.remove('show');});
300});
301setupDrop('notifBtn', 'notifDrop', loadNotifs);
302setupDrop('userBtn', 'userDrop');
303
304
305function loadNotifs() {
306 if (!window.NX || !NX.user) return;
307 var list = document.getElementById('notifList');
308 if (!list) return;
309 fetch(NX.base + '/api/notifications.php')
310 .then(function(r){ return r.json(); })
311 .then(function(rows) {
312 if (!rows.length) { list.innerHTML = '<p class="notif-empty">Nothing new 🎉</p>'; return; }
313 list.innerHTML = rows.map(function(n) {
314 var d = n.payload || {};
315 var text = (n.type === 'reply' && d.title)
316 ? '<strong>@'+esc(d.from)+'</strong> replied in <em>'+esc(d.title)+'</em>'
317 : esc(n.type);
318 var href = (n.type === 'reply' && d.slug) ? NX.base+'/forum/topic.php?slug='+encodeURIComponent(d.slug) : '#';
319 return '<a href="'+href+'" class="notif-item'+(n.read?'':' unread')+'">'
320 +'<div class="notif-item-text">'+text+'</div>'
321 +'<div class="notif-item-time">'+timeAgo(n.created_at)+'</div>'
322 +'</a>';
323 }).join('');
324 })
325 .catch(function(){ if (list) list.innerHTML = '<p class="notif-empty">Failed to load</p>'; });
326}
327var readAllBtn = document.getElementById('readAllBtn');
328if (readAllBtn) {
329 readAllBtn.addEventListener('click', function(){
330 fetch(NX.base + '/api/notifications.php?action=read').then(function(){
331 var dot = document.querySelector('.badge-dot');
332 if (dot) dot.remove();
333 loadNotifs();
334 });
335 });
336}
337
338
339var searchInput = document.getElementById('searchInput');
340var searchResults = document.getElementById('searchResults');
341var searchTimer = null;
342if (searchInput && searchResults) {
343 searchInput.addEventListener('input', function(){
344 clearTimeout(searchTimer);
345 var q = searchInput.value.trim();
346 if (q.length < 2) { searchResults.classList.remove('show'); return; }
347 searchTimer = setTimeout(function(){
348 fetch(NX.base + '/api/search.php?q=' + encodeURIComponent(q))
349 .then(function(r){ return r.json(); })
350 .then(function(d){
351 var topics = Array.isArray(d) ? d : (d.topics || []);
352 var posts = Array.isArray(d) ? [] : (d.posts || []);
353 if (!topics.length && !posts.length) {
354 searchResults.classList.remove('show'); return;
355 }
356 var html = '';
357 if (topics.length) {
358 html += '<div class="sr-section-lbl">Topics</div>';
359 topics.forEach(function(r){
360 html += '<a class="sr-item" href="'+NX.base+'/forum/topic.php?slug='+encodeURIComponent(r.slug)+'">'
361 + '<span class="sr-dot" style="background:'+(r.cat_color||'#3b82f6')+'"></span>'
362 + '<span class="sr-text"><strong>'+esc(r.title)+'</strong>'
363 + '<small>'+esc(r.cat)+'</small></span></a>';
364 });
365 }
366 if (posts.length) {
367 html += '<div class="sr-section-lbl">Matching Posts</div>';
368 posts.forEach(function(r){
369 var preview = (r.content||'').substring(0,65).replace(/\n/g,' ');
370 var url = NX.base+'/forum/topic.php?slug='+encodeURIComponent(r.topic_slug)+'&goto='+r.post_id+'#post-'+r.post_id;
371 html += '<a class="sr-item sr-post" href="'+url+'">'
372 + '<span class="sr-text">'
373 + '<strong>'+esc(r.topic_title)+'</strong>'
374 + '<small>Post #'+r.post_num+' by @'+esc(r.username)+': '+esc(preview)+'…</small>'
375 + '</span>'
376 + '<span class="sr-goto-badge">→</span></a>';
377 });
378 }
379 html += '<a class="sr-item sr-all" href="'+NX.base+'/forum/search.php?q='+encodeURIComponent(q)+'&type=posts">'
380 + '🔍 See all results for “'+esc(q)+'”</a>';
381 searchResults.innerHTML = html;
382 searchResults.classList.add('show');
383 });
384 }, 280);
385 });
386 searchInput.addEventListener('keydown', function(e){
387 if (e.key === 'Enter') window.location = NX.base + '/forum/search.php?q=' + encodeURIComponent(searchInput.value);
388 if (e.key === 'Escape') searchResults.classList.remove('show');
389 });
390 document.addEventListener('click', function(e){
391 if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) searchResults.classList.remove('show');
392 });
393}
394
395
396function fmt(cmd, taId) {
397 var ta = document.getElementById(taId || 'replyTa');
398 if (!ta) return;
399 var s = ta.selectionStart, e = ta.selectionEnd;
400 var sel = ta.value.slice(s, e);
401 var pre = ta.value.slice(0, s);
402 var post = ta.value.slice(e);
403
404
405 if (cmd === 'link') {
406 var url = prompt('Enter URL:');
407 if (!url) return;
408 var txt = sel || 'link text';
409 ta.value = pre + '[' + txt + '](' + url + ')' + post;
410 ta.selectionStart = s + txt.length + url.length + 4;
411 ta.selectionEnd = ta.selectionStart;
412 ta.focus(); ta.dispatchEvent(new Event('input')); return;
413 }
414
415
416
417
418
419
420
421 if (cmd === 'quote') {
422 var text = sel ? sel.replace(/\n+$/, '') : '';
423 var lines = text ? text.split('\n') : ['quoted text'];
424 var quoted = lines.map(function(l) { return '> ' + l; }).join('\n');
425
426
427 var needPre = pre.length > 0 && !/\n\n$/.test(pre) && !pre.endsWith('\n');
428 var before = needPre ? '\n\n' : (pre.length > 0 && !pre.endsWith('\n') ? '\n' : '');
429
430
431 var needPost = post.length > 0 && !post.startsWith('\n\n') && !post.startsWith('\n');
432 var after = needPost ? '\n\n' : '\n';
433
434 ta.value = pre + before + quoted + after + post;
435
436
437 var qStart = s + before.length;
438 var qEnd = qStart + quoted.length;
439 ta.selectionStart = qStart;
440 ta.selectionEnd = qEnd;
441 ta.focus();
442 ta.dispatchEvent(new Event('input'));
443 return;
444 }
445
446
447 var map = {
448 bold: ['**', '**', 'bold text'],
449 italic: ['*', '*', 'italic text'],
450 strike: ['~~', '~~', 'strikethrough'],
451 code: ['```\n', '\n```', 'code here'],
452 codeblock: ['```\n', '\n```', 'code here'],
453 heading: ['## ', '', 'Heading'],
454 ul: ['- ', '', 'list item'],
455 };
456 var c = map[cmd];
457 if (!c) return;
458 var text = sel || c[2];
459 var newText = c[0] + text + c[1];
460 ta.value = pre + newText + post;
461 ta.selectionStart = s + c[0].length;
462 ta.selectionEnd = s + c[0].length + text.length;
463 ta.focus(); ta.dispatchEvent(new Event('input'));
464}
465
466
467function togglePreview() {
468 var ta = document.getElementById('replyTa');
469 var prev = document.getElementById('replyPreview');
470 var btn = document.getElementById('prevBtn');
471 if (!ta || !prev) return;
472 if (prev.classList.contains('hidden')) {
473 prev.innerHTML = md(ta.value);
474 loadTwitterWidgets();
475 prev.classList.remove('hidden');
476 ta.classList.add('hidden');
477 if (btn) btn.classList.add('on');
478 } else {
479 prev.classList.add('hidden');
480 ta.classList.remove('hidden');
481 if (btn) btn.classList.remove('on');
482 }
483}
484
485
486function sendReply(slug) {
487 var ta = document.getElementById('replyTa');
488 if (!ta) return;
489 var content = ta.value.trim();
490 if (!content) { toast('Reply cannot be empty', 'err'); return; }
491
492 var btn = document.getElementById('replyBtn');
493 if (btn) { btn.disabled = true; btn.textContent = 'Posting…'; }
494
495 var fd = new FormData();
496 fd.append('slug', slug);
497 fd.append('content', content);
498 fd.append('csrf', NX.csrf);
499
500
501 var capInp = document.getElementById('postCaptchaInput');
502 if (capInp) fd.append('post_captcha', capInp.value);
503
504 fetch(NX.base + '/api/reply.php', {method:'POST', body:fd})
505 .then(function(r){ return r.json().then(function(d){ d._status=r.status; return d; }); })
506 .then(function(data){
507 if (data.ok) {
508
509 ta.value = '';
510 var prev = document.getElementById('replyPreview');
511 if (prev) prev.classList.add('hidden');
512 ta.classList.remove('hidden');
513 var prevBtn = document.getElementById('prevBtn');
514 if (prevBtn) prevBtn.classList.remove('on');
515 var cnt = document.getElementById('charCnt');
516 if (cnt) { cnt.textContent = '0'; cnt.style.color = ''; }
517
518
519 if (data.new_captcha && capInp) {
520 var ql = capInp.closest('.post-captcha-row')&&capInp.closest('.post-captcha-row').querySelector('.captcha-q');
521 if (ql) ql.textContent = data.new_captcha.q;
522 capInp.value = '';
523 }
524
525
526 var list = document.getElementById('postsList');
527 if (list && data.post) {
528
529 list.insertAdjacentHTML('beforeend', buildPost(data.post));
530 var np = list.lastElementChild;
531 renderTimeAgo();
532 loadTwitterWidgets();
533 np.scrollIntoView({behavior:'smooth',block:'center'});
534 } else { location.reload(); }
535 toast('Reply posted!', 'ok');
536
537 } else if (data.rate_limited) {
538
539 startRateCountdown(data.wait || 30);
540 toast(data.error, 'warn');
541
542 } else if (data.captcha_failed) {
543 toast(data.error, 'err');
544
545 setTimeout(function(){ location.reload(); }, 1500);
546
547 } else {
548 toast(data.error || 'Failed to post', 'err');
549 }
550 })
551 .catch(function(){ toast('Network error — please try again', 'err'); })
552 .finally(function(){ if (btn) { btn.disabled=false; btn.textContent='Post Reply'; } });
553}
554
555
556function startRateCountdown(seconds) {
557 var info = document.getElementById('rateLimitInfo');
558 var btn = document.getElementById('replyBtn');
559 var msg = document.getElementById('rateLimitMsg');
560 var cd = document.getElementById('rateCountdown');
561 if (info) info.style.display = '';
562 if (btn) btn.disabled = true;
563 var remaining = seconds;
564 function tick() {
565 if (msg) msg.textContent = 'Please wait before posting again:';
566 if (cd) cd.textContent = remaining + 's';
567 if (remaining <= 0) {
568 if (info) info.style.display = 'none';
569 if (btn) btn.disabled = false;
570 if (cd) cd.textContent = '';
571 return;
572 }
573 remaining--;
574 setTimeout(tick, 1000);
575 }
576 tick();
577}
578
579function buildPost(p) {
580 var av = p.avatar
581 ? '<img src="'+esc(p.avatar)+'" class="av-lg" alt="">'
582 : '<span class="av-lg av-init">'+esc(p.username[0].toUpperCase())+'</span>';
583 var flair = p.role==='admin' ? '<span class="role-flair admin">Admin</span>'
584 : p.role==='moderator' ? '<span class="role-flair mod">Mod</span>' : '';
585 return '<div class="post" id="post-'+p.id+'">'
586 +'<div class="post-side">'+av
587 +'<a href="'+NX.base+'/users/profile.php?u='+encodeURIComponent(p.username)+'" class="post-name">@'+esc(p.username)+'</a>'
588 +flair+'<span class="post-pcnt">'+p.post_count+' posts</span></div>'
589 +'<div class="post-body"><div class="post-meta-bar"><span class="pnum">#'+p.post_num+'</span>'
590 +'<time class="ago" data-ts="'+esc(p.created_at)+'"></time>'
591 +'<div class="post-acts">'
592 +'<button class="pa-btn" onclick="doLike('+p.id+',this)">♥ <span class="lc">0</span></button>'
593 +'<button class="pa-btn" onclick="doQuote('+p.id+',\''+esc(p.username)+'\')">↩ Reply</button>'
594 +'</div></div>'
595 +'<div class="post-content rendered-post" id="pc-'+p.id+'" data-raw="'+btoa(unescape(encodeURIComponent(p.content)))+'">'
596 +md(p.content)+'</div></div></div>';
597}
598
599
600function doLike(pid, btn) {
601 if (!NX.user) { toast('Log in to like posts', 'warn'); return; }
602 var fd = new FormData(); fd.append('post_id', pid); fd.append('csrf', NX.csrf);
603 fetch(NX.base + '/api/like.php', {method:'POST', body:fd})
604 .then(function(r){ return r.json(); })
605 .then(function(data){
606 if (data.ok) {
607 var lc = btn.querySelector('.lc');
608 btn.classList.toggle('liked', data.liked);
609 if (lc) lc.textContent = data.count;
610 }
611 });
612}
613
614
615function doQuote(pid, uname) {
616 var bodyEl = document.getElementById('pc-'+pid);
617 var text = bodyEl ? bodyEl.innerText.trim().slice(0,300) : '';
618 var ta = document.getElementById('replyTa');
619 if (!ta) return;
620 ta.value = '> **@'+uname+'** wrote:\n> '+text.replace(/\n/g,'\n> ')+'\n\n' + ta.value;
621 ta.focus();
622 ta.scrollIntoView({behavior:'smooth'});
623}
624
625
626function doEdit(pid) {
627 var box = document.getElementById('eb-'+pid);
628 var body = document.getElementById('pc-'+pid);
629 var ta = document.getElementById('et-'+pid);
630 if (!box||!body||!ta) return;
631
632 var rawAttr = body.getAttribute('data-raw');
633 ta.value = rawAttr ? atob(rawAttr) : body.innerText;
634 box.classList.add('visible');
635 box.classList.remove('hidden');
636 body.classList.add('hidden');
637 ta.focus();
638}
639function cancelEdit(pid) {
640 var box = document.getElementById('eb-'+pid);
641 var body = document.getElementById('pc-'+pid);
642 if (box) { box.classList.remove('visible'); box.classList.add('hidden'); }
643 if (body) body.classList.remove('hidden');
644}
645function saveEdit(pid) {
646 var ta = document.getElementById('et-'+pid);
647 var ri = document.getElementById('er-'+pid);
648 if (!ta) return;
649 var fd = new FormData();
650 fd.append('post_id',pid);
651 fd.append('content', ta.value);
652 fd.append('reason', ri ? ri.value : '');
653 fd.append('csrf', NX.csrf);
654 fetch(NX.base+'/api/edit.php', {method:'POST',body:fd})
655 .then(function(r){ return r.json(); })
656 .then(function(data){
657 if (data.ok) {
658 var body = document.getElementById('pc-'+pid);
659 if (body) {
660
661 body.innerHTML = renderMentions(md(data.content));
662
663 try { body.setAttribute('data-raw', btoa(unescape(encodeURIComponent(data.content)))); } catch(e){}
664 body.classList.remove('hidden');
665 loadTwitterWidgets();
666 }
667 cancelEdit(pid);
668 toast('Post updated!','ok');
669 var meta = document.querySelector('#post-'+pid+' .post-meta-bar');
670 if (meta && !meta.querySelector('.edit-lbl')) {
671 var em = document.createElement('em');
672 em.className = 'edit-lbl';
673 em.textContent = ' (edited)';
674 var t = meta.querySelector('time');
675 if (t) t.after(em);
676 }
677 } else {
678 toast(data.error || 'Failed to save', 'err');
679 }
680 })
681 .catch(function(){ toast('Network error', 'err'); });
682}
683
684
685function doDelete(pid) {
686 if (!confirm('Delete this post?')) return;
687 var fd = new FormData(); fd.append('post_id',pid); fd.append('csrf',NX.csrf);
688 fetch(NX.base+'/api/delete.php', {method:'POST',body:fd})
689 .then(function(r){ return r.json(); })
690 .then(function(data){
691 if (data.ok) {
692 var el = document.getElementById('post-'+pid);
693 if (el) { el.style.opacity='.3'; el.style.pointerEvents='none';
694 var b = document.getElementById('pc-'+pid);
695 if (b) b.innerHTML='<em style="color:#94a3b8">This post has been deleted.</em>'; }
696 toast('Deleted','ok');
697 }
698 });
699}
700
701
702function pickImg(inputId, taId) {
703 var inp = document.getElementById(inputId);
704 if (inp) { inp._taId = taId; inp.click(); }
705}
706function uploadImg(inp, taId) {
707 if (inp.files && inp.files[0]) uploadFileToEditor(inp.files[0], taId || inp._taId || 'replyTa');
708}
709function uploadFileToEditor(file, taId) {
710 var ta = document.getElementById(taId || 'replyTa');
711 if (!ta) return;
712
713
714 var maxMb = (NX.maxUploadMb || 5);
715 if (file.size > maxMb * 1024 * 1024) {
716 toast('Image too large — max ' + maxMb + ' MB', 'err');
717 return;
718 }
719
720 var ph = '![Uploading ' + file.name + '…]()';
721 var cur = ta.selectionStart;
722 ta.value = ta.value.slice(0, cur) + ph + ta.value.slice(cur);
723 toast('Uploading…', 'warn');
724
725 var fd = new FormData();
726 fd.append('file', file);
727 fd.append('csrf', NX.csrf);
728
729 fetch(NX.base + '/api/upload.php', { method: 'POST', body: fd })
730 .then(function(r) {
731
732 return r.json().then(function(data) {
733 return { ok: r.ok, data: data };
734 });
735 })
736 .then(function(result) {
737 var data = result.data;
738 if (data.ok) {
739 ta.value = ta.value.replace(ph, '');
740 toast('Image uploaded!', 'ok');
741 } else {
742 ta.value = ta.value.replace(ph, '');
743 toast(data.error || 'Upload failed', 'err');
744 }
745 ta.dispatchEvent(new Event('input'));
746 })
747 .catch(function(err) {
748 ta.value = ta.value.replace(ph, '');
749 toast('Upload failed — check console for details', 'err');
750 console.error('Upload error:', err);
751 });
752}
753
754
755document.addEventListener('DOMContentLoaded', function(){
756 var ta = document.getElementById('replyTa');
757 var cnt = document.getElementById('charCnt');
758 if (ta) {
759 if (cnt) { ta.addEventListener('input', function(){ var n=ta.value.length; cnt.textContent=n; cnt.style.color=n>19000?'var(--red)':n>15000?'var(--amber)':''; }); }
760 ta.addEventListener('paste', function(e){
761 var items = (e.clipboardData || e.originalEvent.clipboardData).items;
762 for (var i=0; i<items.length; i++) {
763 if (items[i].type.indexOf('image')!==-1) { e.preventDefault(); uploadFileToEditor(items[i].getAsFile(),'replyTa'); break; }
764 }
765 });
766 ta.addEventListener('dragover', function(e){ e.preventDefault(); ta.classList.add('dragging'); });
767 ta.addEventListener('dragleave', function(){ ta.classList.remove('dragging'); });
768 ta.addEventListener('drop', function(e){
769 e.preventDefault(); ta.classList.remove('dragging');
770 var files = e.dataTransfer.files;
771 for (var i=0; i<files.length; i++) { if (files[i].type.startsWith('image/')) uploadFileToEditor(files[i],'replyTa'); }
772 });
773 }
774 renderAllMd();
775 renderTimeAgo();
776 setInterval(renderTimeAgo, 60000);
777});
778
779
780var _tc = 0;
781function toast(msg, type) {
782 var col = {ok:'#22c55e',err:'#ef4444',warn:'#f59e0b'}[type||'ok'] || '#3b82f6';
783 var t = document.createElement('div');
784 t.style.cssText = 'position:fixed;bottom:'+(20+_tc*56)+'px;right:20px;'
785 +'background:'+col+';color:#fff;padding:11px 16px;border-radius:8px;'
786 +'font-size:14px;font-family:var(--font,sans-serif);box-shadow:0 4px 16px rgba(0,0,0,.2);'
787 +'z-index:9999;max-width:320px;line-height:1.4;animation:fadeIn .25s ease';
788 t.textContent = msg;
789 document.body.appendChild(t);
790 _tc++;
791 setTimeout(function(){ t.style.transition='opacity .3s'; t.style.opacity='0'; setTimeout(function(){ t.remove(); _tc=Math.max(0,_tc-1); },300); }, 3400);
792}
793
794/* ================================================================
795 @mention autocomplete
796 ================================================================ */
797(function () {
798 var popup = null;
799 var popupItems= [];
800 var popupIdx = -1;
801 var mentionStart = -1;
802 var mentionTA = null;
803
804 function createPopup() {
805 if (popup) return;
806 popup = document.createElement('div');
807 popup.className = 'mention-popup';
808 popup.id = 'mentionPopup';
809 document.body.appendChild(popup);
810 }
811
812 function showPopup(ta, users) {
813 createPopup();
814 popupItems = users;
815 popupIdx = -1;
816 if (!users.length) { hidePopup(); return; }
817 popup.innerHTML = users.map(function(u, i){
818 var av = u.avatar
819 ? '<img src="'+esc(u.avatar)+'" class="av-xs" alt="">'
820 : '<span class="av-xs">'+esc(u.username[0].toUpperCase())+'</span>';
821 return '<div class="mention-item" data-i="'+i+'" onclick="insertMention('+i+')">'+av+'@'+esc(u.username)+'</div>';
822 }).join('');
823 popup.classList.add('show');
824
825
826 var rect = ta.getBoundingClientRect();
827 var coords = getCaretCoords(ta, ta.selectionStart);
828 var top = rect.top + window.scrollY + coords.top + 20;
829 var left = rect.left + window.scrollX + coords.left;
830 popup.style.top = top + 'px';
831 popup.style.left = left + 'px';
832 popup.style.position = 'absolute';
833 }
834
835 function hidePopup() {
836 if (popup) { popup.classList.remove('show'); popup.innerHTML = ''; }
837 mentionStart = -1; mentionTA = null; popupItems = []; popupIdx = -1;
838 }
839
840 window.insertMention = function(idx) {
841 if (!mentionTA || idx < 0 || idx >= popupItems.length) return;
842 var u = popupItems[idx];
843 var val = mentionTA.value;
844 var pre = val.slice(0, mentionStart);
845 var post = val.slice(mentionTA.selectionStart);
846 var ins = '@' + u.username + ' ';
847 mentionTA.value = pre + ins + post;
848 var pos = (pre + ins).length;
849 mentionTA.selectionStart = mentionTA.selectionEnd = pos;
850 mentionTA.focus();
851 hidePopup();
852 };
853
854 function handleMentionKey(e) {
855 if (!popup || !popup.classList.contains('show')) return;
856 if (e.key === 'ArrowDown') {
857 e.preventDefault();
858 popupIdx = (popupIdx + 1) % popupItems.length;
859 updateSelected();
860 } else if (e.key === 'ArrowUp') {
861 e.preventDefault();
862 popupIdx = (popupIdx - 1 + popupItems.length) % popupItems.length;
863 updateSelected();
864 } else if (e.key === 'Enter' || e.key === 'Tab') {
865 if (popupIdx >= 0) { e.preventDefault(); insertMention(popupIdx); }
866 else hidePopup();
867 } else if (e.key === 'Escape') {
868 hidePopup();
869 }
870 }
871
872 function updateSelected() {
873 popup.querySelectorAll('.mention-item').forEach(function(el, i){
874 el.classList.toggle('selected', i === popupIdx);
875 if (i === popupIdx) el.scrollIntoView({block:'nearest'});
876 });
877 }
878
879 var mentionTimer;
880 function handleMentionInput(ta) {
881 var val = ta.value;
882 var caret = ta.selectionStart;
883
884 var before = val.slice(0, caret);
885 var match = before.match(/@([a-zA-Z0-9_\-]*)$/);
886 if (!match) { hidePopup(); return; }
887 var query = match[1];
888 mentionStart = caret - match[0].length;
889 mentionTA = ta;
890 if (query.length < 1) { hidePopup(); return; }
891 clearTimeout(mentionTimer);
892 mentionTimer = setTimeout(function(){
893 fetch(NX.base + '/api/search_users.php?q=' + encodeURIComponent(query))
894 .then(function(r){ return r.json(); })
895 .then(function(users){ showPopup(ta, users); })
896 .catch(function(){ hidePopup(); });
897 }, 200);
898 }
899
900
901 function attachMention(ta) {
902 if (ta.dataset.mentionBound) return;
903 ta.dataset.mentionBound = '1';
904 ta.addEventListener('input', function(){ handleMentionInput(ta); });
905 ta.addEventListener('keydown', handleMentionKey);
906 ta.addEventListener('blur', function(){ setTimeout(hidePopup, 200); });
907 }
908
909 document.addEventListener('DOMContentLoaded', function(){
910 document.querySelectorAll('textarea.reply-ta, textarea.edit-ta').forEach(attachMention);
911
912 new MutationObserver(function(muts){
913 muts.forEach(function(m){
914 m.addedNodes.forEach(function(n){
915 if (n.nodeType!==1) return;
916 n.querySelectorAll && n.querySelectorAll('textarea.reply-ta,textarea.edit-ta').forEach(attachMention);
917 if (n.matches && (n.matches('textarea.reply-ta')||n.matches('textarea.edit-ta'))) attachMention(n);
918 });
919 });
920 }).observe(document.body, {childList:true,subtree:true});
921 });
922
923
924 function getCaretCoords(el, pos) {
925 var div = document.createElement('div');
926 var style = getComputedStyle(el);
927 ['fontFamily','fontSize','fontWeight','lineHeight','padding','border','whiteSpace','wordWrap'].forEach(function(p){
928 div.style[p] = style[p];
929 });
930 div.style.position = 'absolute'; div.style.visibility = 'hidden';
931 div.style.overflow = 'auto'; div.style.width = el.offsetWidth + 'px';
932 var text = el.value.slice(0, pos);
933 div.textContent = text;
934 var span = document.createElement('span');
935 span.textContent = '|';
936 div.appendChild(span);
937 document.body.appendChild(div);
938 var coords = { top: span.offsetTop, left: span.offsetLeft };
939 document.body.removeChild(div);
940 return coords;
941 }
942})();
943
944/* ================================================================
945 @mention rendering in post content
946 Convert @username text → clickable mention links
947 ================================================================ */
948function renderMentions(html) {
949 return html.replace(/@([a-zA-Z0-9_\-]{3,30})/g, function(_, uname) {
950 return '<a href="' + NX.base + '/users/profile.php?u=' + encodeURIComponent(uname) +
951 '" class="mention-tag">@' + esc(uname) + '</a>';
952 });
953}
954
955
956var _origMd = md;
957md = function(raw) {
958 return renderMentions(_origMd(raw));
959};
960
961/* ================================================================
962 Extended notification renderer (friend requests, mentions, messages)
963 ================================================================ */
964var _origLoadNotifs = loadNotifs;
965loadNotifs = function() {
966 if (!window.NX || !NX.user) return;
967 var list = document.getElementById('notifList');
968 if (!list) return;
969 fetch(NX.base + '/api/notifications.php')
970 .then(function(r){ return r.json(); })
971 .then(function(rows){
972 if (!rows.length) { list.innerHTML = '<p class="notif-empty">Nothing new 🎉</p>'; return; }
973 list.innerHTML = rows.map(function(n) {
974 var d = n.payload || {};
975 var text = '';
976 var href = '#';
977 switch (n.type) {
978 case 'reply':
979 text = '<strong>@'+esc(d.from||'')+'</strong> replied in <em>'+esc(d.title||'')+'</em>';
980 if (d.slug) href = NX.base+'/forum/topic.php?slug='+encodeURIComponent(d.slug);
981 break;
982 case 'mention':
983 text = '<strong>@'+esc(d.from||'')+'</strong> mentioned you in a post';
984 if (d.topicSlug) href = NX.base+'/forum/topic.php?slug='+encodeURIComponent(d.topicSlug)+'#post-'+(d.postId||'');
985 break;
986 case 'friend_request':
987 text = '<strong>@'+esc(d.from||'')+'</strong> sent you a friend request';
988 href = NX.base+'/users/profile.php?u='+encodeURIComponent(d.from||'');
989 break;
990 case 'friend_accepted':
991 text = '<strong>@'+esc(d.from||'')+'</strong> accepted your friend request 🎉';
992 href = NX.base+'/users/profile.php?u='+encodeURIComponent(d.from||'');
993 break;
994 case 'message':
995 text = '<strong>@'+esc(d.from||'')+'</strong> sent you a message: <em>'+esc(d.subject||'')+'</em>';
996 href = NX.base+'/messages/';
997 break;
998 case 'karma_admin':
999 var diff = d.change||0;
1000 var sign = diff >= 0 ? '+' : '';
1001 text = 'Your karma was adjusted by an admin: <strong>'+sign+diff+'</strong>'
1002 + (d.new ? ' (now '+d.new+')' : '')
1003 + (d.reason ? ' — <em>'+esc(d.reason)+'</em>' : '');
1004 href = NX.base+'/users/profile.php?u='+encodeURIComponent(NX.user.name||'');
1005 break;
1006 default:
1007 text = esc(n.type);
1008 }
1009 return '<a href="'+href+'" class="notif-item'+(n.read?'':' unread')+'">'
1010 +'<div class="notif-item-text">'+text+'</div>'
1011 +'<div class="notif-item-time">'+timeAgo(n.created_at)+'</div>'
1012 +'</a>';
1013 }).join('');
1014 })
1015 .catch(function(){ if (list) list.innerHTML = '<p class="notif-empty">Failed to load</p>'; });
1016};
1017
1018/* ================================================================
1019 Edit toggle — show edit form only when Edit button clicked
1020 (edit button itself is always visible; form is hidden by default)
1021 ================================================================ */
1022
1023
1024/* ================================================================
1025 Twitter/X widget loader
1026 ================================================================ */
1027function loadTwitterWidgets() {
1028 var tweets = document.querySelectorAll('.embed-tweet[data-tweet-id]:not([data-loaded])');
1029 if (!tweets.length) return;
1030 function doLoad() {
1031 tweets.forEach(function(el) {
1032 el.setAttribute('data-loaded','1');
1033 var id = el.dataset.tweetId;
1034 if (id && window.twttr && window.twttr.widgets) {
1035 window.twttr.widgets.createTweet(id, el, {theme:'light',dnt:true,align:'left'});
1036 }
1037 });
1038 }
1039 if (window.twttr && window.twttr.widgets) { doLoad(); return; }
1040 if (document.querySelector('script[src*="platform.twitter.com"]')) {
1041
1042 var interval = setInterval(function(){
1043 if (window.twttr && window.twttr.widgets) { clearInterval(interval); doLoad(); }
1044 },300);
1045 return;
1046 }
1047 var s = document.createElement('script');
1048 s.src = 'https://platform.twitter.com/widgets.js';
1049 s.async = true;
1050 s.onload = doLoad;
1051 document.head.appendChild(s);
1052}
1053
1054/* ================================================================
1055 Lazy load embeds — for heavy iframes (only load when in viewport)
1056 ================================================================ */
1057document.addEventListener('DOMContentLoaded', function(){
1058 loadTwitterWidgets();
1059
1060
1061 if ('IntersectionObserver' in window) {
1062 var obs = new IntersectionObserver(function(entries){
1063 entries.forEach(function(entry){
1064 if (entry.isIntersecting) {
1065 var iframe = entry.target;
1066 if (iframe.dataset.src) {
1067 iframe.src = iframe.dataset.src;
1068 iframe.removeAttribute('data-src');
1069 obs.unobserve(iframe);
1070 }
1071 }
1072 });
1073 }, {rootMargin:'200px'});
1074
1075 document.querySelectorAll('iframe.embed-yt[data-src]').forEach(function(el){
1076 obs.observe(el);
1077 });
1078 }
1079});
1080
1081/* ================================================================
1082 Post content — use server-rendered HTML, add edit toggle via CSS
1083 edit-box already hidden in CSS (display:none on .edit-box.hidden)
1084 ================================================================ */
1085
1086
1087
1088
1089function copyPostLink(postId, btn) {
1090 var baseUrl = btn.getAttribute('data-url');
1091 var full = window.location.protocol + '//' + window.location.host + baseUrl;
1092 var icon_link = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
1093 var icon_check = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>';
1094 function showCopied() {
1095 btn.innerHTML = icon_check;
1096 btn.title = 'Copied!';
1097 setTimeout(function(){ btn.innerHTML = icon_link; btn.title = 'Copy link to this post'; }, 2000);
1098 toast('Post link copied!', 'ok');
1099 }
1100 if (navigator.clipboard && navigator.clipboard.writeText) {
1101 navigator.clipboard.writeText(full).then(showCopied).catch(function() {
1102 prompt('Copy this link:', full);
1103 });
1104 } else {
1105 prompt('Copy this link:', full);
1106 }
1107}
1108
1109
1110function cbCopy(btn) {
1111
1112 var wrap = btn.closest('.code-block-wrap');
1113 if (!wrap) return;
1114 var code = wrap.querySelector('pre.code-block code');
1115 if (!code) return;
1116
1117 var text = code.innerText !== undefined ? code.innerText : code.textContent;
1118
1119 var ok = function() {
1120 var orig = btn.innerHTML;
1121 btn.classList.add('copied');
1122 btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" '
1123 + 'stroke-linecap="round" stroke-linejoin="round" width="14" height="14">'
1124 + '<polyline points="20 6 9 17 4 12"/></svg><span>Copied!</span>';
1125 setTimeout(function() {
1126 btn.classList.remove('copied');
1127 btn.innerHTML = orig;
1128 }, 2000);
1129 };
1130
1131 if (navigator.clipboard && navigator.clipboard.writeText) {
1132 navigator.clipboard.writeText(text).then(ok).catch(function() {
1133 fallbackCopy(text, ok);
1134 });
1135 } else {
1136 fallbackCopy(text, ok);
1137 }
1138}
1139
1140function fallbackCopy(text, cb) {
1141 var ta = document.createElement('textarea');
1142 ta.value = text;
1143 ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0';
1144 document.body.appendChild(ta);
1145 ta.focus();
1146 ta.select();
1147 try { document.execCommand('copy'); if (cb) cb(); }
1148 catch(e) {}
1149 document.body.removeChild(ta);
1150}
1151
1152
1153
1154
1155(function () {
1156
1157 var DANGEROUS = [
1158 /<script/i,
1159 /<iframe/i,
1160 /<object/i,
1161 /<embed/i,
1162 /<form/i,
1163 /<base\s/i,
1164 /<link\s/i,
1165 /<meta\s/i,
1166 /<svg[\s>]/i,
1167 /<\?php/i,
1168 /<\?=/,
1169 /javascript\s*:/i,
1170 /vbscript\s*:/i,
1171 /on\w+\s*=/i,
1172 /data\s*:\s*text\/html/i,
1173 ];
1174
1175 function checkContent(ta) {
1176 var val = ta.value;
1177 for (var i = 0; i < DANGEROUS.length; i++) {
1178 if (DANGEROUS[i].test(val)) {
1179 showHtmlWarning(ta);
1180 return;
1181 }
1182 }
1183 hideHtmlWarning(ta);
1184 }
1185
1186 function showHtmlWarning(ta) {
1187 var id = 'sec-warn-' + ta.id;
1188 if (document.getElementById(id)) return;
1189 var box = document.createElement('div');
1190 box.id = id;
1191 box.className = 'sec-warning';
1192 box.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '
1193 + 'stroke-linecap="round" stroke-linejoin="round" width="15" height="15">'
1194 + '<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>'
1195 + '<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
1196 + '<span>Raw HTML/code detected. For security, HTML is stripped on save. '
1197 + 'Use ``` code blocks ``` to display code.</span>';
1198 ta.parentNode.insertBefore(box, ta.nextSibling);
1199 }
1200
1201 function hideHtmlWarning(ta) {
1202 var id = 'sec-warn-' + ta.id;
1203 var el = document.getElementById(id);
1204 if (el) el.remove();
1205 }
1206
1207 function attachTo(taId) {
1208 var ta = document.getElementById(taId);
1209 if (!ta) return;
1210 ta.addEventListener('input', function () { checkContent(ta); });
1211 ta.addEventListener('paste', function () {
1212 setTimeout(function () { checkContent(ta); }, 10);
1213 });
1214 }
1215
1216
1217 document.addEventListener('DOMContentLoaded', function () {
1218 attachTo('replyTa');
1219 });
1220
1221
1222 window.attachSecCheck = attachTo;
1223})();
1224
1225
1226function wgToggle() {
1227 var b = document.querySelector('.wg-body');
1228 var btn = document.querySelector('.wg-toggle');
1229 if (!b) return;
1230 var open = b.style.display === 'none';
1231 b.style.display = open ? '' : 'none';
1232 if (btn) {
1233 btn.setAttribute('aria-expanded', open ? 'true' : 'false');
1234 btn.classList.toggle('wg-open', open);
1235 }
1236}