1/** posting.js: iterating over posts and adding new ones to the thread */ 2var posting = {}; 3 4posting.init = function() { 5 6 api.noReportCaptcha = !document.getElementById('divReportCaptcha'); 7 posting.idsRelation = {}; 8 posting.highLightedIds = []; 9 10 posting.postCellTemplate = api.getTemplate("thread-post-template"); 11 posting.uploadCell = api.getTemplate("thread-upload-template"); 12 13 var omissionIndicator = posting.postCellTemplate.template 14 .getElementsByClassName("contentOmissionIndicator")[0]; 15 if (omissionIndicator) 16 omissionIndicator.remove(); 17 18 posting.localTime = localStorage.getItem("localTime") === "true"; 19 posting.relativeTime = localStorage.getItem("relativeTime") === "true"; 20 21 /*TODO figure out what this was preventing 22 if (typeof (thread) !== 'undefined') { 23 return; 24 }*/ 25 26 if (api.boardUri) { 27 const key = `${api.boardUri}-yous`; 28 const update = () => { 29 const yousRaw = localStorage.getItem(key); 30 posting.yous = yousRaw === null ? [] : JSON.parse(yousRaw); 31 } 32 addEventListener("storage", (e) => { 33 if (e.key !== key) return; 34 update(); 35 }); 36 update(); 37 } 38 39 posting.existingPosts = Array.from(document.getElementsByClassName('linkSelf')) 40 .map((linkSelf) => posting.parseExistingPost(linkSelf)).filter((post) => (post !== undefined)); 41 42 if (posting.relativeTime) { 43 posting.updateAllRelativeTimes(); 44 setInterval(posting.updateAllRelativeTimes, 1000 * 60 * 5); 45 } 46}; 47 48/** 49 * extract the name, tripcode, and ID (if enabled) from a post 50 * @param {HTMLElement} innerPost 51 * @param {api.parsePostLink.ret} postInfo 52 */ 53posting.getExtraInfo = function(innerPost, postInfo) { 54 var name = innerPost.getElementsByClassName('linkName')[0].textContent; 55 56 if (name.indexOf('#') >= 0) { 57 postInfo.trip = name.substring(name.lastIndexOf('#') + 1); 58 postInfo.name = name.substring(0, name.indexOf('#')); 59 } else { 60 postInfo.name = name; 61 } 62 63 postInfo.subject = innerPost.getElementsByClassName('labelSubject')[0]?.textContent; 64 65 var labelId = innerPost.getElementsByClassName('labelId')[0]; 66 67 if (labelId) { 68 postInfo.id = labelId.textContent; 69 } 70} 71 72/** 73 * parse (and modify) an existing post in the thread; adds tooltips, dropdowns, 74 * etc if scripts are loaded 75 * @param {HTMLAnchorElement} linkSelf 76 * @param {boolean} [noExtras] 77 * @param {boolean} [noModify] 78 * @param {boolean} [noTooltips] 79 * @returns {posting.parseExistingPost.ret} 80 */ 81posting.parseExistingPost = function(linkSelf, noExtras, noModify, noTooltips) { 82 if (posting.postCellTemplate.template.contains(linkSelf)) { 83 return; 84 } 85 86 const innerPost = linkSelf.closest(".innerPost, .innerOP"); 87 var postInfo = api.parsePostLink(linkSelf.href); 88 posting.getExtraInfo(innerPost, postInfo); 89 90 var ret = {}; 91 ret.postInfo = postInfo; 92 ret.linkSelf = linkSelf; 93 ret.innerPost = innerPost; 94 ret.files = innerPost.querySelector(".panelUploads"); 95 ret.message = innerPost.querySelector(".divMessage"); 96 97 if (noModify) 98 return; 99 100 //cache a clone of the node with alt before it gets additional backlinks 101 // NOTE: It's pretty expensive and should be replaced with parsing instead. 102 if (typeof tooltips !== "undefined" && !noTooltips) { 103 const post = KnownPost.getOrInsert(linkSelf.href); 104 post.linkElem(innerPost); 105 post.setDead(false); 106 if (posting.yous?.includes(+post.id)) post.setYou(true); 107 if (tooltips.bottomBacklinks) { 108 const container = innerPost.querySelector(".panelBacklinks"); 109 innerPost.append(container); 110 } 111 innerPost.dataset.uri = post.uri; 112 tooltips.renderedPostCache.set(post.uri, innerPost.cloneNode(true)); 113 } 114 115 //update with local times 116 var labelCreated = innerPost.getElementsByClassName('labelCreated')[0]; 117 if (posting.localTime) { 118 posting.setLocalTime(labelCreated); 119 } 120 121 if (posting.relativeTime) { 122 posting.addRelativeTime(labelCreated); 123 } 124 125 //thumbnail hovering/hiding 126 if (typeof thumbs !== "undefined") { 127 Array.from(innerPost.getElementsByClassName('uploadCell')) 128 .forEach((cell) => thumbs.processUploadCell(cell)); 129 } 130 131 if (typeof embed !== "undefined") { 132 Array.from(ret.message.getElementsByTagName("a")) 133 .forEach((embedLink) => embed.processLinkForEmbed(embedLink)); 134 } 135 136 if (!noExtras) 137 posting.addExternalExtras(ret, noTooltips); 138 139 if (typeof hiding !== "undefined" && !noTooltips) { 140 hiding.check(ret); 141 } 142 143 return ret; 144}; 145 146/** 147 * @param {posting.parseExistingPost.ret} ret 148 * @param {boolean} noTooltips 149 */ 150posting.addExternalExtras = function(ret, noTooltips) { 151 var innerPost = ret.innerPost; 152 var linkSelf = ret.linkSelf; 153 var postInfo = ret.postInfo; 154 155 posting.processIdLabel(innerPost.getElementsByClassName("labelId")[0]); 156 157 posting.processAttachmentLinks(innerPost); 158 159 //(You)s 160 if (posting.yous && posting.yous.indexOf(+postInfo.post) !== -1) { 161 posting.markPostAsYou(innerPost); 162 } 163 164 //load posting menu, hiding menu, and watcher 165 //TODO: coalesce files? 166 if (typeof postingMenu !== "undefined") { 167 interfaceUtils.addMenuDropdown(ret, "Post Menu", 168 "extraMenuButton", postingMenu.buildMenu); 169 } 170 171 if (typeof hiding !== "undefined") { 172 interfaceUtils.addMenuDropdown(ret, "Hide", 173 "hideButton", hiding.buildMenu); 174 } 175 176 if (typeof watcher !== "undefined" && !noTooltips) { 177 if (postInfo.op) 178 watcher.processOP(innerPost); 179 } 180 181 if (typeof qr !== "undefined") { 182 /** @type {HTMLAnchorElement} */ 183 const linkQuote = innerPost.querySelector(".linkQuote"); 184 const id = linkQuote.hash.substring(2); 185 linkQuote.addEventListener("click", () => qr.showQr(id, innerPost)); 186 } 187 188 if (typeof tooltips !== "undefined") { 189 tooltips.processQuotes(innerPost, !noTooltips); 190 } 191}; 192 193/** 194 * @param {HTMLElement} time 195 */ 196posting.setLocalTime = function(time) { 197 198 time.textContent = api.formatDateToDisplay( 199 new Date(time.textContent + ' UTC'), true); 200 201}; 202 203posting.updateAllRelativeTimes = function() { 204 205 var times = document.getElementsByClassName('labelCreated'); 206 207 for (var i = 0; i < times.length; i++) { 208 posting.addRelativeTime(times[i]); 209 } 210 211}; 212 213/** 214 * @param {HTMLElement} time 215 */ 216posting.addRelativeTime = function(time) { 217 218 var timeObject = new Date(time.textContent + (posting.localTime ? '' : ' UTC')); 219 220 var relativeTime = time.nextElementSibling; 221 if (relativeTime.className !== 'relativeTime') { 222 223 relativeTime = document.createElement('span'); 224 225 relativeTime.className = 'relativeTime'; 226 227 time.parentNode.insertBefore(relativeTime, time.nextSibling); 228 229 } 230 231 var now = new Date(); 232 233 var content; 234 235 var delta = now - timeObject; 236 237 var second = 1000; 238 var minute = second * 60; 239 var hour = minute * 60; 240 var day = hour * 24;
241 var month = day * 30.5; 242 var year = day * 365.25; 243 244 if (delta > 2 * year) { 245 content = Math.ceil(delta / year) + ' years ago'; 246 } else if (delta > 2 * month) { 247 content = Math.ceil(delta / month) + ' months ago'; 248 } else if (delta > 2 * day) { 249 content = Math.ceil(delta / day) + ' days ago'; 250 } else if (delta > 2 * hour) { 251 content = Math.ceil(delta / hour) + ' hours ago'; 252 } else if (delta > 2 * minute) { 253 content = Math.ceil(delta / minute) + ' minutes ago'; 254 } else { 255 content = 'Just now'; 256 } 257 258 relativeTime.textContent = ' (' + content + ')'; 259 260}; 261 262/** 263 * @param {HTMLElement} label 264 */ 265posting.processIdLabel = function(label) { 266 if (!label) return; 267 268 const id = label.textContent; 269 const array = posting.idsRelation[id] || []; 270 const innerPost = label.closest(".innerPost, .innerOP"); 271 272 if (!innerPost.classList.contains("clone")) { 273 posting.idsRelation[id] = array; 274 array.push(innerPost); 275 } 276 277 label.onmouseover = function() { 278 label.textContent = `${id} (${array.length})`; 279 } 280 281 label.onmouseout = function() { 282 label.textContent = id; 283 } 284 285 label.onclick = function() { 286 287 var index = posting.highLightedIds.indexOf(id); 288 window.location.hash = '_'; 289 290 if (index > -1) { 291 posting.highLightedIds.splice(index, 1); 292 } else { 293 posting.highLightedIds.push(id); 294 } 295 296 for (var i = 0; i < array.length; i++) { 297 var cellToChange = array[i]; 298 299 if (cellToChange.className === 'innerOP') { 300 continue; 301 } 302 303 /*? 'innerPost' : 'markedPost';*/ 304 cellToChange.classList.toggle("markedPost", index === -1); 305 } 306 307 }; 308 309}; 310 311posting.downloadAttachment = async function(url, abortController, onProgress) { 312 313 try { 314 const response = await fetch(url, { signal: abortController.signal }); 315 if (!response.ok) throw new Error(`HTTP response status: ${response.status}`); 316 317 const totalLength = parseInt(response.headers.get('Content-Length')); 318 if (!totalLength) throw new Error(`Content-Length is 0 or missing.`); 319 320 const reader = response.body.getReader(); 321 const chunks = []; 322 let loaded = 0; 323 324 while (true) { 325 const { done, value } = await reader.read(); 326 327 if (done) break; 328 329 chunks.push(value); 330 loaded += value.length; 331 332 if (onProgress && totalLength) { 333 onProgress(loaded, totalLength); 334 } 335 } 336 337 return new Blob(chunks); 338 } catch (err) { 339 if (err.name === 'AbortError') { 340 console.log('Download aborted by user'); 341 } else { 342 console.log('Download failed: ', err); 343 } 344 } 345} 346 347// a virtual link to "click" on to download file 348posting.virtualLinkElement = document.createElement('a'); 349 350posting.attachmentClickHandler = function() { 351 let abortController = null; 352 353 return async function(e) { 354 e.preventDefault(); 355 356 const link = e.currentTarget; 357 const details = link.closest('details'); 358 const imgLink = details.querySelector('.imgLink'); 359 const img = imgLink.querySelector('img'); 360 const imgWidth = img.getBoundingClientRect().width; 361 362 let pb = details.querySelector('.download-progress-bar'); 363 364 if (!pb) { 365 pb = document.createElement('div'); 366 pb.classList.add('download-progress-bar'); 367 pb.style.maxWidth = imgWidth + 'px'; 368 } 369 370 // if not null = download in progress, need to cancel download 371 if (abortController) { 372 abortController.abort(); 373 abortController = null; 374 375 imgLink.style.pointerEvents = 'all'; 376 377 pb.remove(); 378 379 return; 380 } 381 382 img.style.filter = 'opacity(0.5)'; 383 imgLink.style.pointerEvents = 'none'; 384 imgLink.insertBefore(pb, img); 385 386 abortController = new AbortController(); 387 388 let totalSize = null; 389 390 const blob = await posting.downloadAttachment(link.href, abortController, (loaded, total) => { 391 totalSize = totalSize ?? api.formatFileSize(total); 392 393 pb.style.setProperty('--progress', `${(loaded / total) * imgWidth}px`); 394 395 pb.dataset.loaded = api.formatFileSize(loaded); 396 pb.dataset.total = totalSize; 397 pb.dataset.percentage = ((loaded / total) * 100).toFixed(2); 398 }); 399 400 abortController = null; 401 imgLink.style.pointerEvents = 'all'; 402 img.style.removeProperty('filter'); 403 404 if (!blob) return; 405 406 pb.remove(); 407 408 const url = URL.createObjectURL(blob); 409 410 posting.virtualLinkElement.href = url; 411 posting.virtualLinkElement.download = link.download; 412 posting.virtualLinkElement.click(); 413 414 URL.revokeObjectURL(url); 415 } 416} 417 418posting.processAttachmentLinks = function (innerPost) { 419 const links = innerPost.querySelectorAll('.originalNameLink'); 420 421 if (!links) return; 422 423 for (const link of links) { 424 link.addEventListener('click', posting.attachmentClickHandler()); 425 } 426} 427 428/** 429 * @param {ThreadJson.ThreadOrPost} post 430 * @param {HTMLElement} cell 431 */ 432posting.setLastEditedLabel = function(post, cell) { 433 434 var editedLabel = cell.getElementsByClassName('labelLastEdit')[0]; 435 436 if (post.lastEditTime) { 437 438 var formatedDate = api.formatDateToDisplay(new Date(post.lastEditTime)); 439 440 editedLabel.textContent = 441 `Edited last time by ${post.lastEditLogin} on ${formatedDate}.`; 442 443 } else { 444 editedLabel.remove(); 445 } 446 447}; 448 449/** 450 * @param {HTMLElement} cell 451 * @param {*} file 452 */ 453posting.setUploadLinks = (cell, file) => { 454 const thumbLink = /** @type {HTMLAnchorElement} */ ( 455 cell.getElementsByClassName("imgLink")[0] 456 ); 457 thumbLink.href = file.path; 458 if (file.width !== null && file.height !== null) {
459 thumbLink.dataset.filewidth = file.width; 460 thumbLink.dataset.fileheight = file.height; 461 } 462 thumbLink.dataset.filemime = file.mime; 463 464 // match behavior in moe_modifications/prevent_reflow.js 465 const thumbSize = 220; // should be informed by the backend 466 const thumbDims = { width: thumbSize, height: thumbSize }; 467 // magic number from Lynxchan common.getImgTag 468 if (file.thumb.length > 71) { // has unique thumbnail 469 if (file.width === null || file.height === null) { 470 // the backend can sometimes report null even though it generates a thumbnail (#261) 471 thumbDims.width = null; 472 thumbDims.height = null; 473 } else if (file.width < thumbSize && file.height < thumbSize) { 474 thumbDims.width = file.width; 475 thumbDims.height = file.height; 476 } else if (file.width > file.height) { 477 thumbDims.height = Math.round((file.height / file.width) * thumbSize); 478 } else if (file.height > file.width) { 479 thumbDims.width = Math.round((file.width / file.height) * thumbSize); 480 } 481 } else { // has generic or spoiler thumbnail 482 thumbDims.width = 125; 483 thumbDims.height = 125; 484 } 485 486 const thumbImg = document.createElement("img"); 487 // make js thumbnails match html thumbnails 100% 488 // this does not actually make it lazy 489 thumbImg.loading = "lazy"; 490 thumbImg.width = thumbDims.width; 491 thumbImg.height = thumbDims.height; 492 thumbImg.src = file.thumb; 493 494 thumbLink.appendChild(thumbImg); 495 496 const sauceLink = /** @type {HTMLAnchorElement} */ ( 497 cell.getElementsByClassName("sauceLink")[0] 498 ); 499 sauceLink.href = 'https://saucenao.com/search.php?url=https://capybarachan.org' + file.path; 500 501 const nameLink = /** @type {HTMLAnchorElement} */ ( 502 cell.getElementsByClassName("nameLink")[0] 503 ); 504 nameLink.href = file.path; 505 506 const originalNameLink = /** @type {HTMLAnchorElement} */ ( 507 cell.getElementsByClassName("originalNameLink")[0] 508 ); 509 // NOTE originalName is HTML formatted by the backend 510 originalNameLink.innerHTML = file.originalName; 511 originalNameLink.href = file.path; 512 originalNameLink.setAttribute("download", file.originalName); 513}; 514 515/** 516 * @param {HTMLElement} node 517 * @param {ThreadJson.ThreadOrPost} post 518 * @param {string} boardUri 519 */ 520posting.setUploadCells = (node, post, boardUri) => { 521 for (const [index, file] of post.files.entries()) { 522 const cell = document.createElement("figure"); 523 cell.className = "uploadCell"; 524 posting.uploadCell.cloneInto(cell); 525 526 posting.setUploadLinks(cell, file); 527 528 cell.getElementsByClassName("sizeLabel")[0].textContent = 529 api.formatFileSize(file.size); 530 531 const dimensionLabel = cell.getElementsByClassName("dimensionLabel")[0]; 532 533 if (file.width !== null && file.height !== null) { 534 dimensionLabel.textContent = `${file.width}x${file.height}`; 535 } else { 536 dimensionLabel.remove(); 537 } 538 539 const unlinkCell = /** @type {HTMLAnchorElement} */ ( 540 cell.getElementsByClassName("unlinkLink")[0] 541 ); 542 const deleteCell = /** @type {HTMLAnchorElement} */ ( 543 cell.getElementsByClassName("unlinkAndDeleteLink")[0] 544 ); 545 546 if (api.mod) { 547 const params = new URLSearchParams([ 548 ["boardUri", boardUri], 549 [post.postId ? "postId" : "threadId", post.postId || post.threadId], 550 ["index", index.toString()], 551 ]); 552 unlinkCell.href = `/unlinkSingle.js?${params.toString()}`; 553 554 params.append("delete", "1"); 555 deleteCell.href = `/unlinkSingle.js?${params.toString()}`; 556 } else { 557 unlinkCell.remove(); 558 deleteCell.remove(); 559 } 560 561 if (file.sha256) { 562 cell.getElementsByClassName("labelHash")[0].textContent = file.sha256; 563 } else { 564 cell.getElementsByClassName("divHash")[0].remove(); 565 } 566 567 node.appendChild(cell); 568 } 569}; 570 571/** 572 * @param {HTMLElement} postCell 573 * @param {ThreadJson.ThreadOrPost} post 574 * @param {boolean} [noExtras] 575 */ 576posting.setPostHideableElements = function(postCell, post, noExtras) { 577 578 var subjectLabel = postCell.getElementsByClassName('labelSubject')[0]; 579 580 if (post.subject) { 581 subjectLabel.textContent = post.subject; 582 } else { 583 subjectLabel.remove(); 584 } 585 586 if (post.id) { 587 var labelId = postCell.getElementsByClassName('labelId')[0]; 588 labelId.setAttribute('style', 'background-color: #' + post.id); 589 labelId.textContent = post.id; 590 } else { 591 var spanId = postCell.getElementsByClassName('spanId')[0]; 592 spanId.remove(); 593 } 594 595 var banMessageLabel = postCell.getElementsByClassName('divBanMessage')[0]; 596 597 if (!post.banMessage) { 598 banMessageLabel.parentNode.removeChild(banMessageLabel); 599 } else { 600 //XXX review whether ban messages contain preformatted HTML 601 banMessageLabel.innerHTML = post.banMessage; 602 } 603 604 posting.setLastEditedLabel(post, postCell); 605 606 /** @type {HTMLImageElement} */ 607 const imgFlag = postCell.querySelector(".imgFlag"); 608 609 if (post.flag) { 610 imgFlag.src = post.flag; 611 imgFlag.title = post.flagName.replace(/&(l|g)t;/g, ([, c]) => c === "l" ? "<" : ">"); 612 613 if (post.flagCode) { 614 imgFlag.classList.add(`flag${post.flagCode}`); 615 } 616 } else { 617 imgFlag.remove(); 618 } 619 620 if (!post.asn) { 621 postCell.getElementsByClassName('panelASN')[0].remove(); 622 } else { 623 postCell.getElementsByClassName('labelASN')[0].textContent = post.asn; 624 } 625 626 if (!post.bypassId) { 627 postCell.getElementsByClassName('panelBypassId')[0].remove(); 628 } else { 629 postCell.getElementsByClassName('labelBypassId')[0].textContent = post.bypassId; 630 } 631 632 if (!post.ip) { 633 postCell.getElementsByClassName('panelIp')[0].remove(); 634 } else { 635 636 postCell.getElementsByClassName('labelIp')[0].textContent = post.ip; 637 638 if (!post.broadRange) { 639 postCell.getElementsByClassName('panelRange')[0].remove(); 640 } else { 641 642 postCell.getElementsByClassName('labelBroadRange')[0].textContent = post.broadRange; 643 postCell.getElementsByClassName('labelNarrowRange')[0].textContent = post.narrowRange; 644 645 } 646 647 } 648 649}; 650 651/** 652 * @param {HTMLElement} postCell 653 * @param {ThreadJson.ThreadOrPost} post 654 * @param {string} boardUri 655 * @param {HTMLAnchorElement} link 656 * @param {string} threadId 657 * @param {HTMLAnchorElement} linkQuote 658 * @param {HTMLElement} deletionCheckbox 659 * @param {boolean} [preview] 660 */ 661posting.setPostLinks = function(postCell, post, boardUri, link, threadId, 662 linkQuote, deletionCheckbox, preview) { 663 664 var postingId = post.postId || threadId; 665 666 preview = preview || api.isBoard; 667 668 var linkStart = (preview ? '/' + boardUri + '/res/' + threadId + '.html' : '') 669 + '#'; 670 671 linkQuote.href = linkStart; 672 link.href = linkStart; 673 674 link.href += postingId; 675 linkQuote.href += 'q' + postingId; 676 677 var linkEdit = postCell.getElementsByClassName('linkEdit')[0]; 678 var linkHistory = postCell.getElementsByClassName('linkHistory')[0]; 679 var linkFileHistory = postCell.getElementsByClassName('linkFileHistory')[0]; 680 var linkOffenseHistory = postCell.getElementsByClassName('linkOffenseRecord')[0]; 681 682 var complement = (post.postId ? 'postId' : 'threadId') + '=' + postingId; 683 684 if (api.mod) { 685 linkEdit.href = '/edit.js?boardUri=' + boardUri + '&'; 686 linkEdit.href += complement; 687 } else if (linkEdit) { 688 linkEdit.remove(); 689 } 690 691 if (api.mod && (post.ip || post.bypassId)) { 692 linkFileHistory.href = '/mediaManagement.js?boardUri=' + boardUri + '&'; 693 linkFileHistory.href += complement; 694 695 linkHistory.href = '/latestPostings.js?boardUri=' + boardUri + '&'; 696 linkHistory.href += complement; 697 698 linkOffenseHistory.href = '/offenseRecord.js?boardUri=' + boardUri + '&'; 699 linkOffenseHistory.href += complement; 700 701 } else if (linkHistory) { 702 linkHistory.remove(); 703 linkFileHistory.remove(); 704 linkOffenseHistory.remove(); 705 } 706 707 var checkboxName = boardUri + '-' + threadId; 708 709 if (post.postId) { 710 checkboxName += '-' + post.postId; 711 } 712 713 deletionCheckbox.setAttribute('name', checkboxName); 714 715}; 716 717/** 718 * @param {HTMLElement} postingCell 719 * @param {ThreadJson.ThreadOrPost} posting 720 */ 721posting.setRoleSignature = function(postingCell, posting) { 722 723 var labelRole = postingCell.getElementsByClassName('labelRole')[0]; 724 725 if (posting.signedRole) { 726 labelRole.textContent = posting.signedRole; 727 } else { 728 labelRole.parentNode.removeChild(labelRole); 729 } 730 731}; 732 733/** 734 * @param {HTMLElement} postCell 735 * @param {ThreadJson.ThreadOrPost} post 736 * @param {string} boardUri 737 * @param {string} threadId 738 * @param {boolean} [noExtras] 739 * @param {boolean} [preview] 740 */ 741posting.setPostComplexElements = function(postCell, post, boardUri, threadId, 742 noExtras, preview) { 743 744 posting.setRoleSignature(postCell, post); 745 746 var link = postCell.getElementsByClassName('linkSelf')[0]; 747 748 var linkQuote = postCell.getElementsByClassName('linkQuote')[0]; 749 linkQuote.textContent = post.postId || threadId; 750
751 var deletionCheckbox = postCell.getElementsByClassName('deletionCheckBox')[0]; 752 753 posting.setPostLinks(postCell, post, boardUri, link, threadId, linkQuote, 754 deletionCheckbox, preview); 755 756 var panelUploads = postCell.getElementsByClassName('panelUploads')[0]; 757 758 if (!post.files || !post.files.length) { 759 panelUploads.remove(); 760 } else { 761 762 if (post.files.length > 1) { 763 panelUploads.className += ' multipleUploads'; 764 } 765 766 posting.setUploadCells(panelUploads, post, boardUri); 767 } 768 769}; 770 771/** 772 * @param {ThreadJson.ThreadOrPost} post 773 * @param {string} boardUri 774 * @param {string} threadId 775 * @param {boolean} [noExtras] Not used. 776 * @param {boolean} [preview] Used by latestPostings mod feature. 777 * @param {boolean} [noParse] Used by tooltips. 778 * @returns {HTMLElement} 779 */ 780posting.addPost = function(post, boardUri, threadId, noExtras, preview, noParse) { 781 782 var postCell = document.createElement('div'); 783 posting.postCellTemplate.cloneInto(postCell); 784 785 if (api.mod) { 786 if (post.trash !== true) { 787 postCell.querySelector('.trashIndicator').remove(); 788 } 789 } 790 else { 791 postCell.querySelector('.trashIndicator').remove(); 792 } 793 794 if (post.locked === true) { 795 api.addIndicator('lockIndicator', 'Locked', postCell); 796 } 797 if (post.archived === true) { 798 api.addIndicator('archiveIndicator', 'Archived', postCell); 799 } 800 if (post.pinned === true) { 801 api.addIndicator('pinIndicator', 'Sticky', postCell); 802 } 803 if (post.cyclic === true) { 804 api.addIndicator('cyclicIndicator', 'Cyclical Thread', postCell); 805 } 806 if (post.autoSage === true) { 807 api.addIndicator('bumpLockIndicator', 'Autosage', postCell); 808 } 809 810 postCell.dataset.boarduri = boardUri; 811 812 if (post.postId) { 813 postCell.id = post.postId; 814 postCell.className = 'postCell'; 815 } else { 816 postCell.id = post.threadId; 817 postCell.className = 'opCell'; 818 819 const innerPart = postCell.getElementsByClassName('innerPost')[0]; 820 innerPart.classList.remove('innerPost'); 821 innerPart.classList.add('innerOP'); 822 823 const panelUploads = postCell.getElementsByClassName('panelUploads')[0]; 824 panelUploads.classList.add('opUploadPanel'); 825 826 const title = postCell.getElementsByClassName('title')[0]; 827 title.classList.remove('postInfo'); 828 title.classList.add('opHead'); 829 830 innerPart.insertBefore(panelUploads, title); 831 } 832 833 var labelBoard = postCell.getElementsByClassName('labelBoard')[0]; 834 835 if (preview) { 836 labelBoard.textContent = '/' + boardUri + '/'; 837 } else { 838 labelBoard.remove(); 839 } 840 841 var linkName = postCell.getElementsByClassName('linkName')[0]; 842 843 linkName.textContent = post.name; 844 845 if (post.email) { 846 linkName.href = 'mailto:' + post.email; 847 } else { 848 linkName.className += ' noEmailName'; 849 } 850 851 var labelCreated = postCell.getElementsByClassName('labelCreated')[0]; 852 853 labelCreated.textContent = api.formatDateToDisplay(new Date(post.creation), false); 854 855 if (posting.relativeTime) 856 posting.addRelativeTime(labelCreated); 857 858 postCell.getElementsByClassName('divMessage')[0].innerHTML = post.markdown; 859 860 posting.setPostHideableElements(postCell, post, noExtras); 861 862 posting.setPostComplexElements(postCell, post, boardUri, threadId, noExtras, 863 preview); 864 865 if (!noParse) { 866 /** @type {HTMLAnchorElement} */ 867 const linkSelf = postCell.querySelector(".linkSelf"); 868 const existParse = posting.parseExistingPost(linkSelf, noExtras); 869 870 if (!noExtras) { 871 posting.existingPosts.push(existParse); 872 if (typeof gallery !== 'undefined') gallery.addPost(post); 873 } 874 } 875 876 return postCell; 877 878}; 879 880/** 881 * @param {HTMLElement} innerPost 882 * @param {boolean} [unmark] 883 */ 884posting.markPostAsYou = function(innerPost, unmark = false) { 885 const author = innerPost.querySelector(".linkName"); 886 if (!unmark) { 887 author.classList.add("youName"); 888 innerPost.classList.add("yourPost"); 889 } else { 890 author.classList.remove("youName"); 891 innerPost.classList.remove("yourPost"); 892 } 893}; 894 895posting.init();
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.