1(function () { 2 'use strict'; 3 4 var API = '/pintrading/api'; 5 6 function lang() { 7 return document.body.getAttribute('data-guide-lang') || document.documentElement.lang || 'en'; 8 } 9 10 var STR = { 11 en: { 12 lead: 'Sign in to join the discussion. New posts are reviewed by server admins before they appear.', 13 loginDiscord: 'Discord', 14 loginGoogle: 'Google', 15 logout: 'Sign out', 16 placeholder: 'Write a commentâ¦', 17 hint: 'Markdown: **bold**, *italic*, `code`, [label](https://â¦)', 18 submit: 'Submit', 19 cancelReply: 'Cancel reply', 20 reply: 'Reply', 21 replyingTo: 'Replying to thread', 22 none: 'No comments yet. Be the first to share a tip or a question.', 23 modTitle: 'Moderation queue', 24 modEmpty: 'No comments awaiting approval.', 25 approve: 'Approve', 26 reject: 'Reject', 27 removePublic: 'Remove', 28 removePublicConfirm: 'Remove this comment from the public page?', 29 modHintGoogleOnly: 30 'Moderation is tied to Discord. Sign out and sign in with Discord to use the moderation queue.', 31 modHintDiscordApi: 32 'You are on Discord, but moderator access failed. Enable Server Members Intent for the bot in the Discord Developer Portal, or ask the site owner to list your Discord user ID in extra_moderator_user_ids.', 33 pendingNote: 'Comments appear after moderator approval.', 34 submitted: 'Thanks. Your comment was sent for review.', 35 errorGeneric: 'Something went wrong. Please try again.', 36 authOk: 'You are signed in.', 37 authErr: 'Sign-in failed.', 38 signedInVia: 'Signed in', 39 }, 40 fr: { 41 lead: 'Connectez-vous pour participer. Les nouveaux messages sont vérifiés par les admins du serveur avant publication.', 42 loginDiscord: 'Discord', 43 loginGoogle: 'Google', 44 logout: 'Se déconnecter', 45 placeholder: 'Ãcrire un commentaireâ¦', 46 hint: 'Markdown : **gras**, *italique*, `code`, [texte](https://â¦)', 47 submit: 'Envoyer', 48 cancelReply: 'Annuler la réponse', 49 reply: 'Répondre', 50 replyingTo: 'Réponse dans le fil', 51 none: 'Pas encore de commentaires.', 52 modTitle: 'File de modération', 53 modEmpty: 'Aucun commentaire en attente.', 54 approve: 'Approuver', 55 reject: 'Refuser', 56 removePublic: 'Retirer', 57 removePublicConfirm: 'Retirer ce commentaire de la page publique ?', 58 modHintGoogleOnly: 59 'La modération est liée à Discord. Déconnectez-vous et reconnectez-vous avec Discord pour utiliser la file.', 60 modHintDiscordApi: 61 'Vous êtes sur Discord, mais lâaccès modérateur a échoué. Activez lâintent « Server Members » pour le bot dans le portail développeur Discord, ou demandez dâajouter votre ID utilisateur dans extra_moderator_user_ids.', 62 pendingNote: 'Les commentaires apparaissent après validation par un modérateur.', 63 submitted: 'Merci. Votre commentaire a été envoyé pour relecture.', 64 errorGeneric: 'Une erreur est survenue. Réessayez.', 65 authOk: 'Vous êtes connecté·e.', 66 authErr: 'Connexion impossible.', 67 signedInVia: 'Connecté via', 68 }, 69 nl: { 70 lead: 'Log in om mee te praten. Nieuwe berichten worden door serveradmins goedgekeurd voordat ze zichtbaar zijn.', 71 loginDiscord: 'Discord', 72 loginGoogle: 'Google', 73 logout: 'Uitloggen', 74 placeholder: 'Schrijf een reactieâ¦', 75 hint: 'Markdown: **vet**, *cursief*, `code`, [tekst](https://â¦)', 76 submit: 'Versturen', 77 cancelReply: 'Antwoord annuleren', 78 reply: 'Antwoorden', 79 replyingTo: 'Antwoord in draad', 80 none: 'Nog geen reacties.', 81 modTitle: 'Moderatiewachtrij', 82 modEmpty: 'Geen reacties in afwachting.', 83 approve: 'Goedkeuren', 84 reject: 'Afwijzen', 85 removePublic: 'Verwijderen', 86 removePublicConfirm: 'Deze reactie van de openbare pagina verwijderen?', 87 modHintGoogleOnly: 88 'Moderatie hangt aan Discord. Log uit en log opnieuw in met Discord om de moderatiewachtrij te gebruiken.', 89 modHintDiscordApi: 90 'Je bent op Discord, maar moderatorrechten lukken niet. Zet « Server Members Intent » aan voor de bot in het Discord Developer Portal, of laat je Discord user ID in extra_moderator_user_ids zetten.', 91 pendingNote: 'Reacties verschijnen na goedkeuring door een moderator.', 92 submitted: 'Bedankt. Je reactie is ter beoordeling verstuurd.', 93 errorGeneric: 'Er ging iets mis. Probeer opnieuw.', 94 authOk: 'Je bent ingelogd.', 95 authErr: 'Inloggen mislukt.', 96 signedInVia: 'Ingelogd via', 97 }, 98 }; 99 100 function t(key) { 101 var pack = STR[lang()] || STR.en; 102 return pack[key] || STR.en[key] || key; 103 } 104 105 function qs(sel, root) { 106 return (root || document).querySelector(sel); 107 } 108 109 function escapeHtml(s) { 110 return String(s) 111 .replace(/&/g, '&') 112 .replace(/</g, '<') 113 .replace(/>/g, '>') 114 .replace(/"/g, '"'); 115 } 116 117 function formatTime(ts) { 118 try {
119 return new Date(ts * 1000).toLocaleString(lang(), { 120 dateStyle: 'medium', 121 timeStyle: 'short', 122 }); 123 } catch (e) { 124 return ''; 125 } 126 } 127 128 function apiGet(path) { 129 return fetch(API + path, { credentials: 'same-origin' }).then(function (r) { 130 return r.text().then(function (txt) { 131 var j = {}; 132 try { 133 j = txt ? JSON.parse(txt) : {}; 134 } catch (e) { 135 j = {}; 136 } 137 if (!r.ok) throw new Error(j.error || txt.slice(0, 120) || 'HTTP ' + r.status); 138 return j; 139 }); 140 }); 141 } 142 143 function apiPost(path, body, csrf) { 144 return fetch(API + path, { 145 method: 'POST', 146 credentials: 'same-origin', 147 headers: { 148 'Content-Type': 'application/json', 149 'X-CSRF-Token': csrf, 150 }, 151 body: JSON.stringify(body), 152 }).then(function (r) { 153 return r.text().then(function (txt) { 154 var j = {}; 155 try { 156 j = txt ? JSON.parse(txt) : {}; 157 } catch (e) { 158 j = {}; 159 } 160 if (!r.ok) throw new Error(j.error || txt.slice(0, 120) || 'HTTP ' + r.status); 161 return j; 162 }); 163 }); 164 } 165 166 var state = { 167 user: null, 168 moderator: false, 169 csrf: '', 170 parentId: null, 171 providersEnabled: {}, 172 activeProvider: null, 173 }; 174 175 function setFlash(kind, msg) { 176 var el = qs('#comments-flash'); 177 if (!el) return; 178 el.className = 'guide-comments-flash guide-comments-flash--' + kind; 179 el.textContent = msg; 180 el.hidden = false; 181 if (kind === 'ok') { 182 setTimeout(function () { 183 el.hidden = true; 184 }, 6000); 185 } 186 } 187 188 function consumeAuthQuery() { 189 var u = new URL(window.location.href); 190 var a = u.searchParams.get('auth'); 191 if (a === 'ok') { 192 setFlash('ok', t('authOk')); 193 u.searchParams.delete('auth'); 194 history.replaceState({}, '', u.pathname + u.search + u.hash); 195 } else if (a && /^err_/.test(a)) { 196 setFlash('err', t('authErr')); 197 u.searchParams.delete('auth'); 198 history.replaceState({}, '', u.pathname + u.search + u.hash); 199 } 200 } 201 202 function isAllowedAvatarUrl(url) { 203 if (!url || typeof url !== 'string') return false; 204 try { 205 var u = new URL(url, window.location.origin); 206 if (u.protocol !== 'https:') return false; 207 var host = u.hostname.toLowerCase(); 208 if (host === 'cdn.discordapp.com') { 209 var p = u.pathname; 210 return p.indexOf('/avatars/') === 0 || p.indexOf('/embed/avatars/') === 0; 211 } 212 if (host.indexOf('googleusercontent.com') !== -1) { 213 return true; 214 } 215 return false; 216 } catch (e) { 217 return false; 218 } 219 } 220 221 function safeAvatarUrl(url) { 222 return isAllowedAvatarUrl(url) 223 ? url 224 : 'https://cdn.discordapp.com/embed/avatars/0.png'; 225 } 226 227 function isSafeCommentLink(url) { 228 try { 229 var u = new URL(url, window.location.origin); 230 return u.protocol === 'https:' || u.protocol === 'http:'; 231 } catch (e) { 232 return false; 233 } 234 } 235 236 function appendSanitizedNode(source, target) { 237 if (source.nodeType === Node.TEXT_NODE) { 238 target.appendChild(document.createTextNode(source.textContent || '')); 239 return; 240 } 241 if (source.nodeType !== Node.ELEMENT_NODE) return; 242 243 var tag = source.tagName.toLowerCase(); 244 var allowed = tag === 'p' || tag === 'br' || tag === 'strong' || tag === 'em' || tag === 'code' || tag === 'a'; 245 if (!allowed) { 246 for (var child = source.firstChild; child; child = child.nextSibling) { 247 appendSanitizedNode(child, target); 248 } 249 return; 250 } 251 252 var el = document.createElement(tag); 253 if (tag === 'a') { 254 var href = source.getAttribute('href') || ''; 255 if (!isSafeCommentLink(href)) { 256 for (var linkChild = source.firstChild; linkChild; linkChild = linkChild.nextSibling) { 257 appendSanitizedNode(linkChild, target); 258 } 259 return; 260 } 261 el.href = href; 262 el.target = '_blank'; 263 el.rel = 'nofollow ugc noopener noreferrer'; 264 } 265 for (var nested = source.firstChild; nested; nested = nested.nextSibling) { 266 appendSanitizedNode(nested, el); 267 } 268 target.appendChild(el); 269 } 270 271 function setCommentHtml(host, html) { 272 var template = document.createElement('template'); 273 template.innerHTML = String(html || ''); 274 while (host.firstChild) host.removeChild(host.firstChild); 275 for (var child = template.content.firstChild; child; child = child.nextSibling) { 276 appendSanitizedNode(child, host); 277 } 278 } 279 280 function safeTimestamp(value) { 281 var ts = Number(value); 282 return Number.isFinite(ts) && ts > 0 ? ts : 0; 283 } 284 285 function oauthNextPath() { 286 var next = window.location.pathname || '/pintrading/'; 287 if (!next.startsWith('/pintrading')) next = '/pintrading/'; 288 if (next === '/pintrading') next = '/pintrading/'; 289 return next; 290 } 291 292 function authStartHref(provider) { 293 return API + '/auth/' + provider + '?next=' + encodeURIComponent(oauthNextPath()); 294 } 295 296 function providerLabel(name) { 297 var labels = { discord: 'Discord', google: 'Google' }; 298 return labels[name] || name; 299 } 300 301 function renderAuth() { 302 var host = qs('#comments-auth'); 303 if (!host) return; 304 host.innerHTML = ''; 305 306 if (state.user) { 307 var row = document.createElement('div'); 308 row.className = 'guide-comments-user'; 309 var img = document.createElement('img'); 310 img.src = safeAvatarUrl(state.user.avatar_url); 311 img.alt = ''; 312 img.width = 40; 313 img.height = 40; 314 img.loading = 'lazy'; 315 var meta = document.createElement('div'); 316 meta.className = 'guide-comments-user-meta'; 317 var name = document.createElement('span'); 318 name.textContent = state.user.username; 319 meta.appendChild(name); 320 if (state.activeProvider) { 321 var via = document.createElement('span'); 322 via.className = 'guide-comments-user-via'; 323 via.textContent = t('signedInVia') + ' ' + providerLabel(state.activeProvider); 324 meta.appendChild(via); 325 } 326 row.appendChild(img); 327 row.appendChild(meta); 328 var out = document.createElement('button'); 329 out.type = 'button'; 330 out.className = 'guide-comments-btn guide-comments-btn--ghost'; 331 out.textContent = t('logout'); 332 out.addEventListener('click', function () { 333 out.disabled = true; 334 apiPost('/logout', { csrf: state.csrf }, state.csrf) 335 .then(function () { 336 state.user = null; 337 state.moderator = false;
338 state.activeProvider = null; 339 return refreshSession(); 340 }) 341 .then(function () { 342 renderAuth(); 343 renderCompose(); 344 if (!state.moderator) renderMod([]); 345 }) 346 .catch(function () { 347 setFlash('err', t('errorGeneric')); 348 }) 349 .finally(function () { 350 out.disabled = false; 351 }); 352 }); 353 host.appendChild(row); 354 host.appendChild(out); 355 if (!state.moderator) { 356 var prov = (state.user && state.user.providers) || []; 357 var hasDiscord = prov.indexOf('discord') !== -1; 358 var mh = document.createElement('p'); 359 mh.className = 'guide-comments-hint guide-comments-hint--full'; 360 mh.textContent = hasDiscord ? t('modHintDiscordApi') : t('modHintGoogleOnly'); 361 host.appendChild(mh); 362 } 363 return; 364 } 365 366 var pe = state.providersEnabled || {}; 367 var anyProvider = pe.discord || pe.google; 368 if (!anyProvider) { 369 var warn = document.createElement('p'); 370 warn.className = 'guide-comments-hint'; 371 warn.textContent = t('pendingNote'); 372 host.appendChild(warn); 373 return; 374 } 375 376 var actions = document.createElement('div'); 377 actions.className = 'guide-comments-auth-actions'; 378 379 function addOAuthBtn(provider, labelKey) { 380 var a = document.createElement('a'); 381 a.className = 'guide-comments-btn guide-comments-btn--' + provider; 382 a.href = authStartHref(provider === 'discord' ? 'login' : provider); 383 a.textContent = t(labelKey); 384 a.setAttribute('aria-label', t(labelKey)); 385 actions.appendChild(a); 386 } 387 388 if (pe.discord) addOAuthBtn('discord', 'loginDiscord'); 389 if (pe.google) addOAuthBtn('google', 'loginGoogle'); 390 391 host.appendChild(actions); 392 393 var note = document.createElement('span'); 394 note.className = 'guide-comments-hint'; 395 note.style.margin = '0'; 396 note.textContent = t('pendingNote'); 397 host.appendChild(note); 398 } 399 400 function renderCompose() { 401 var box = qs('#comments-compose'); 402 if (!box) return; 403 if (!state.user) { 404 box.innerHTML = '<p class="guide-comments-hint">' + escapeHtml(t('pendingNote')) + '</p>'; 405 return; 406 } 407 box.innerHTML = 408 '<input type="text" id="comments-hp" class="guide-comments-hp" tabindex="-1" autocomplete="off" aria-hidden="true">' + 409 '<div id="comments-reply-hint" class="guide-comments-replying" hidden></div>' + 410 '<label for="comments-body">' + 411 escapeHtml(t('placeholder')) + 412 '</label>' + 413 '<textarea id="comments-body" maxlength="4000" rows="5"></textarea>' + 414 '<p class="guide-comments-hint">' + 415 escapeHtml(t('hint')) + 416 '</p>' + 417 '<div class="guide-comments-compose-actions">' + 418 '<button type="button" class="guide-comments-btn" id="comments-submit">' + 419 escapeHtml(t('submit')) + 420 '</button>' + 421 '<button type="button" class="guide-comments-btn guide-comments-btn--ghost" id="comments-cancel-reply" hidden>' + 422 escapeHtml(t('cancelReply')) + 423 '</button></div>' + 424 '<p class="guide-comments-status" id="comments-compose-status" hidden></p>'; 425 426 var ta = qs('#comments-body', box); 427 var submit = qs('#comments-submit', box); 428 var cancel = qs('#comments-cancel-reply', box); 429 var rh = qs('#comments-reply-hint', box); 430 var st = qs('#comments-compose-status', box); 431 432 function updateReplyUi() { 433 if (state.parentId) { 434 rh.hidden = false; 435 rh.textContent = t('replyingTo') + ' (#' + state.parentId + ')'; 436 cancel.hidden = false; 437 } else { 438 rh.hidden = true; 439 cancel.hidden = true; 440 } 441 } 442 updateReplyUi(); 443 444 cancel.addEventListener('click', function () { 445 state.parentId = null; 446 updateReplyUi(); 447 }); 448 449 submit.addEventListener('click', function () { 450 var hpEl = qs('#comments-hp', box); 451 if (hpEl && hpEl.value) { 452 return; 453 } 454 var body = ((ta && ta.value) || '').trim(); 455 if (!body) return; 456 submit.disabled = true; 457 st.hidden = true; 458 apiPost( 459 '/comments', 460 { body: body, parent_id: state.parentId, csrf: state.csrf, hp: hpEl ? hpEl.value : '' }, 461 state.csrf 462 ) 463 .then(function () { 464 ta.value = ''; 465 state.parentId = null; 466 updateReplyUi(); 467 st.textContent = t('submitted'); 468 st.hidden = false;
469 return loadComments(); 470 }) 471 .then(function () { 472 if (state.moderator) return loadMod(); 473 }) 474 .catch(function (e) { 475 setFlash('err', e.message || t('errorGeneric')); 476 }) 477 .finally(function () { 478 submit.disabled = false; 479 }); 480 }); 481 } 482 483 function renderComment(node) { 484 var author = (node && node.author) || {}; 485 var createdAt = safeTimestamp(node && node.created_at); 486 var li = document.createElement('li'); 487 li.className = 'guide-comment'; 488 var card = document.createElement('article'); 489 card.className = 'guide-comment-card'; 490 card.setAttribute('itemscope', ''); 491 card.setAttribute('itemtype', 'https://schema.org/Comment'); 492 493 var img = document.createElement('img'); 494 img.src = safeAvatarUrl(author.avatar_url); 495 img.alt = ''; 496 img.width = 44; 497 img.height = 44; 498 img.loading = 'lazy'; 499 500 var body = document.createElement('div'); 501 body.className = 'guide-comment-body'; 502 503 var meta = document.createElement('div'); 504 meta.className = 'guide-comment-meta'; 505 var strong = document.createElement('strong'); 506 strong.setAttribute('itemprop', 'author'); 507 strong.textContent = author.username || 'User'; 508 var time = document.createElement('time'); 509 if (createdAt) time.dateTime = new Date(createdAt * 1000).toISOString(); 510 time.textContent = createdAt ? formatTime(createdAt) : ''; 511 meta.appendChild(strong); 512 meta.appendChild(time); 513 514 var text = document.createElement('div'); 515 text.className = 'guide-comment-text'; 516 text.setAttribute('itemprop', 'text'); 517 setCommentHtml(text, node && node.body_html); 518 519 var actions = document.createElement('div'); 520 actions.className = 'guide-comment-actions'; 521 if (state.user) { 522 var rb = document.createElement('button'); 523 rb.type = 'button'; 524 rb.textContent = t('reply'); 525 rb.addEventListener('click', function () { 526 state.parentId = node.id; 527 renderCompose(); 528 var ta2 = qs('#comments-body'); 529 if (ta2) ta2.focus(); 530 }); 531 actions.appendChild(rb); 532 } 533 534 if (state.moderator) { 535 var rm = document.createElement('button'); 536 rm.type = 'button'; 537 rm.className = 'guide-comment-remove'; 538 rm.textContent = t('removePublic'); 539 rm.addEventListener('click', function () { 540 if (!window.confirm(t('removePublicConfirm'))) return; 541 rm.disabled = true; 542 apiPost('/moderation/remove', { id: node.id, csrf: state.csrf }, state.csrf) 543 .then(function () { 544 return Promise.all([loadComments(), loadMod()]); 545 }) 546 .catch(function (e) { 547 setFlash('err', e.message || t('errorGeneric')); 548 }) 549 .finally(function () { 550 rm.disabled = false; 551 }); 552 }); 553 actions.appendChild(rm); 554 } 555 556 body.appendChild(meta); 557 body.appendChild(text); 558 body.appendChild(actions); 559 card.appendChild(img); 560 card.appendChild(body); 561 li.appendChild(card); 562 563 if (node.children && node.children.length) { 564 var ul = document.createElement('ul'); 565 ul.className = 'guide-comments-list'; 566 for (var i = 0; i < node.children.length; i++) { 567 ul.appendChild(renderComment(node.children[i])); 568 } 569 li.appendChild(ul); 570 } 571 return li; 572 } 573 574 function renderList(tree) { 575 var host = qs('#comments-list'); 576 if (!host) return; 577 host.innerHTML = ''; 578 if (!tree || !tree.length) { 579 var p = document.createElement('p'); 580 p.className = 'guide-comments-hint'; 581 p.textContent = t('none'); 582 host.appendChild(p); 583 return; 584 } 585 var ul = document.createElement('ul'); 586 ul.className = 'guide-comments-list'; 587 for (var j = 0; j < tree.length; j++) { 588 ul.appendChild(renderComment(tree[j])); 589 } 590 host.appendChild(ul); 591 } 592 593 function renderMod(pending) { 594 var host = qs('#comments-mod'); 595 if (!host) return; 596 if (!state.moderator) { 597 host.innerHTML = ''; 598 host.hidden = true; 599 return; 600 } 601 host.hidden = false;
602 host.innerHTML = '<h3>' + escapeHtml(t('modTitle')) + '</h3><div id="comments-mod-inner"></div>'; 603 var inner = qs('#comments-mod-inner', host); 604 if (!pending || !pending.length) { 605 inner.innerHTML = '<p class="guide-comments-mod-empty">' + escapeHtml(t('modEmpty')) + '</p>'; 606 return; 607 } 608 for (var i = 0; i < pending.length; i++) { 609 inner.appendChild(renderModItem(pending[i])); 610 } 611 } 612 613 function renderModItem(item) { 614 var author = (item && item.author) || {}; 615 var createdAt = safeTimestamp(item && item.created_at); 616 var wrap = document.createElement('div'); 617 wrap.className = 'guide-comments-mod-item'; 618 var head = document.createElement('div'); 619 head.className = 'guide-comments-mod-head'; 620 var img = document.createElement('img'); 621 img.src = safeAvatarUrl(author.avatar_url); 622 img.alt = ''; 623 var who = document.createElement('strong'); 624 who.textContent = author.username || 'User'; 625 var meta = document.createElement('span'); 626 meta.style.fontSize = '12px'; 627 meta.style.color = 'var(--text-muted)'; 628 var providerHint = ''; 629 if (author.providers && author.providers.length) { 630 providerHint = ' · ' + author.providers.join(', '); 631 } 632 meta.textContent = (createdAt ? formatTime(createdAt) : '') + ' · #' + item.id + providerHint; 633 head.appendChild(img); 634 head.appendChild(who); 635 head.appendChild(meta); 636 637 var prev = document.createElement('div'); 638 prev.className = 'guide-comments-mod-preview'; 639 setCommentHtml(prev, item && item.body_html); 640 641 var actions = document.createElement('div'); 642 actions.className = 'guide-comments-mod-actions'; 643 var ap = document.createElement('button'); 644 ap.type = 'button'; 645 ap.className = 'approve'; 646 ap.textContent = t('approve'); 647 var rj = document.createElement('button'); 648 rj.type = 'button'; 649 rj.className = 'reject'; 650 rj.textContent = t('reject'); 651 652 function act(status) { 653 ap.disabled = rj.disabled = true; 654 apiPost('/moderation/' + status, { id: item.id, csrf: state.csrf }, state.csrf) 655 .then(function () { 656 wrap.remove(); 657 return Promise.all([loadComments(), loadMod()]); 658 }) 659 .catch(function (e) { 660 setFlash('err', e.message || t('errorGeneric')); 661 }) 662 .finally(function () { 663 ap.disabled = rj.disabled = false; 664 }); 665 } 666 ap.addEventListener('click', function () { 667 act('approve'); 668 }); 669 rj.addEventListener('click', function () { 670 act('reject'); 671 }); 672 actions.appendChild(ap); 673 actions.appendChild(rj); 674 675 wrap.appendChild(head); 676 wrap.appendChild(prev); 677 wrap.appendChild(actions); 678 return wrap; 679 } 680 681 function refreshSession() { 682 return apiGet('/session').then(function (d) { 683 state.user = d.user; 684 state.moderator = !!d.moderator; 685 state.csrf = d.csrf || ''; 686 state.providersEnabled = d.providers_enabled || {}; 687 state.activeProvider = d.active_provider || null; 688 return d; 689 }); 690 } 691 692 function loadComments() { 693 return apiGet('/comments').then(function (d) { 694 renderList(d.comments || []); 695 }); 696 } 697 698 function loadMod() { 699 if (!state.moderator) { 700 renderMod([]); 701 return Promise.resolve(); 702 } 703 return apiGet('/moderation/pending').then(function (d) { 704 renderMod(d.pending || []); 705 }); 706 } 707 708 function init() { 709 var root = qs('#comments'); 710 if (!root) return; 711 712 consumeAuthQuery(); 713 714 refreshSession() 715 .then(function () { 716 renderAuth(); 717 renderCompose(); 718 return loadComments(); 719 }) 720 .then(function () { 721 return loadMod(); 722 }) 723 .catch(function () { 724 setFlash('err', t('errorGeneric')); 725 }); 726 } 727 728 if (document.readyState === 'loading') { 729 document.addEventListener('DOMContentLoaded', init); 730 } else { 731 init(); 732 } 733})();
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.