PageSourceSearch

https://capybarachan.org/.static/js/posting.js

js capybarachan.org collected 2026-10-08 10:12:20 UTC 24,916 bytes, 895 lines download raw bytes

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.