mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 08:22:24 +00:00
1293 lines
36 KiB
JavaScript
1293 lines
36 KiB
JavaScript
import { escapeHTML } from '../utils/domUtils.js';
|
||
|
||
/**
|
||
* Responsibilities mirrored from the legacy DS component that ships inside
|
||
* blog.html:
|
||
*
|
||
* - Normalise anchors coming from HTTP URLs, note/nevent IDs, or naddr
|
||
* references.
|
||
* - Resolve relay inputs (with defaults) and build the correct Nostr filter for
|
||
* whichever anchor type we are showing.
|
||
* - Hydrate the thread from cache, refresh it from the network, and keep the
|
||
* subscription lifecycle tidy so repeated mounts don’t leak listeners.
|
||
* - Expose cache clearing, watermark/title rendering, and anchor version updates
|
||
* to the host document while dispatching mount/unmount events for observers.
|
||
*/
|
||
|
||
function createSvg(pathD, { viewBox, className } = {}) {
|
||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
if (viewBox) {
|
||
svg.setAttribute('viewBox', viewBox);
|
||
}
|
||
if (className) {
|
||
svg.setAttribute('class', className);
|
||
}
|
||
|
||
if (pathD) {
|
||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||
path.setAttribute('d', pathD);
|
||
svg.appendChild(path);
|
||
}
|
||
|
||
return svg;
|
||
}
|
||
|
||
export function createZapthreadsCommentInfoPicture() {
|
||
const container = document.createElement('div');
|
||
container.className = 'ztr-comment-info-picture';
|
||
|
||
const image = document.createElement('img');
|
||
container.appendChild(image);
|
||
|
||
return container;
|
||
}
|
||
|
||
export function createZapthreadsReplyForm() {
|
||
const form = document.createElement('div');
|
||
form.className = 'ztr-reply-form';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
form.appendChild(textarea);
|
||
|
||
const controls = document.createElement('div');
|
||
controls.className = 'ztr-reply-controls';
|
||
form.appendChild(controls);
|
||
|
||
return form;
|
||
}
|
||
|
||
export function createZapthreadsPublishingDisabledMessage() {
|
||
const span = document.createElement('span');
|
||
span.textContent = 'Publishing is disabled';
|
||
return span;
|
||
}
|
||
|
||
export function createZapthreadsReplyErrorMessage() {
|
||
const span = document.createElement('span');
|
||
span.className = 'ztr-reply-error';
|
||
span.textContent = 'Error: ';
|
||
return span;
|
||
}
|
||
|
||
export function createZapthreadsSpinner() {
|
||
const svg = createSvg(null, {
|
||
viewBox: '0 0 50 50',
|
||
className: 'ztr-spinner',
|
||
});
|
||
|
||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||
circle.setAttribute('class', 'path');
|
||
circle.setAttribute('cx', '25');
|
||
circle.setAttribute('cy', '25');
|
||
circle.setAttribute('r', '20');
|
||
circle.setAttribute('fill', 'none');
|
||
circle.setAttribute('stroke-width', '5');
|
||
svg.appendChild(circle);
|
||
|
||
return svg;
|
||
}
|
||
|
||
export function createZapthreadsReplyAsButton() {
|
||
const button = document.createElement('button');
|
||
button.className = 'ztr-reply-button';
|
||
button.textContent = 'Reply as ';
|
||
return button;
|
||
}
|
||
|
||
export function createZapthreadsReplyAnonymouslyButton() {
|
||
const button = document.createElement('button');
|
||
button.className = 'ztr-reply-button';
|
||
button.textContent = 'Reply anonymously';
|
||
return button;
|
||
}
|
||
|
||
export function createZapthreadsReplyLoginButton() {
|
||
const button = document.createElement('button');
|
||
button.className = 'ztr-reply-login-button';
|
||
button.textContent = 'Log in';
|
||
return button;
|
||
}
|
||
|
||
export function createZapthreadsLikeAction() {
|
||
const item = document.createElement('li');
|
||
item.className = 'ztr-comment-action-like';
|
||
|
||
const span = document.createElement('span');
|
||
span.textContent = ' likes';
|
||
item.appendChild(span);
|
||
|
||
return item;
|
||
}
|
||
|
||
export function createZapthreadsZapAction() {
|
||
const item = document.createElement('li');
|
||
item.className = 'ztr-comment-action-zap';
|
||
|
||
const span = document.createElement('span');
|
||
span.textContent = ' sats';
|
||
item.appendChild(span);
|
||
|
||
return item;
|
||
}
|
||
|
||
export function createZapthreadsNewCommentContainer() {
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'ztr-comment-new';
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'ztr-comment-body';
|
||
wrapper.appendChild(body);
|
||
|
||
const actions = document.createElement('ul');
|
||
actions.className = 'ztr-comment-actions';
|
||
body.appendChild(actions);
|
||
|
||
return wrapper;
|
||
}
|
||
|
||
export function createZapthreadsThreadContainer() {
|
||
const div = document.createElement('div');
|
||
div.className = 'ztr-thread';
|
||
return div;
|
||
}
|
||
|
||
export function createZapthreadsReplyActionItem() {
|
||
const item = document.createElement('li');
|
||
item.className = 'ztr-comment-action-reply';
|
||
|
||
const span = document.createElement('span');
|
||
item.appendChild(span);
|
||
|
||
return item;
|
||
}
|
||
|
||
export function createZapthreadsCommentContainer() {
|
||
const root = document.createElement('div');
|
||
root.className = 'ztr-comment';
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'ztr-comment-body';
|
||
root.appendChild(body);
|
||
|
||
const infoWrapper = document.createElement('div');
|
||
infoWrapper.className = 'ztr-comment-info-wrapper';
|
||
body.appendChild(infoWrapper);
|
||
|
||
const info = document.createElement('div');
|
||
info.className = 'ztr-comment-info';
|
||
infoWrapper.appendChild(info);
|
||
|
||
info.appendChild(createZapthreadsCommentInfoPicture());
|
||
|
||
const infoItems = document.createElement('ul');
|
||
infoItems.className = 'ztr-comment-info-items';
|
||
info.appendChild(infoItems);
|
||
|
||
const authorItem = document.createElement('li');
|
||
authorItem.className = 'ztr-comment-info-author';
|
||
infoItems.appendChild(authorItem);
|
||
|
||
const authorLink = document.createElement('a');
|
||
authorLink.target = '_blank';
|
||
authorItem.appendChild(authorLink);
|
||
|
||
const nowrap = document.createElement('span');
|
||
nowrap.className = 'ztr-nowrap';
|
||
const strong = document.createElement('strong');
|
||
strong.textContent = ' <!>ed';
|
||
nowrap.appendChild(strong);
|
||
nowrap.appendChild(document.createTextNode(' '));
|
||
authorItem.appendChild(nowrap);
|
||
|
||
const dotsItem = document.createElement('li');
|
||
infoItems.appendChild(dotsItem);
|
||
const dotsLink = document.createElement('a');
|
||
dotsLink.className = 'ztr-comment-info-dots';
|
||
dotsItem.appendChild(dotsLink);
|
||
|
||
const metaList = document.createElement('ul');
|
||
metaList.className = 'ztr-comment-info-items';
|
||
infoWrapper.appendChild(metaList);
|
||
|
||
const truncatedText = document.createElement('div');
|
||
truncatedText.className = 'ztr-comment-text';
|
||
body.appendChild(truncatedText);
|
||
|
||
const fullText = document.createElement('div');
|
||
fullText.className = 'ztr-comment-text';
|
||
body.appendChild(fullText);
|
||
|
||
const actions = document.createElement('ul');
|
||
actions.className = 'ztr-comment-actions';
|
||
body.appendChild(actions);
|
||
|
||
return root;
|
||
}
|
||
|
||
export function createZapthreadsBulletSeparator() {
|
||
const item = document.createElement('li');
|
||
item.textContent = '●';
|
||
return item;
|
||
}
|
||
|
||
export function createZapthreadsRepliesLabel() {
|
||
const item = document.createElement('li');
|
||
item.textContent = ' repl';
|
||
return item;
|
||
}
|
||
|
||
export function createZapthreadsActionCountItem() {
|
||
const item = document.createElement('li');
|
||
const span = document.createElement('span');
|
||
item.appendChild(span);
|
||
return item;
|
||
}
|
||
|
||
export function createZapthreadsInfoPane() {
|
||
const container = document.createElement('div');
|
||
container.className = 'ztr-info-pane';
|
||
|
||
const link = document.createElement('a');
|
||
link.target = '_blank';
|
||
container.appendChild(link);
|
||
|
||
const small = document.createElement('small');
|
||
small.textContent = 'Event data';
|
||
link.appendChild(small);
|
||
|
||
return container;
|
||
}
|
||
|
||
export function createZapthreadsCrossPostWarning() {
|
||
const paragraph = document.createElement('p');
|
||
paragraph.className = 'warning';
|
||
|
||
const span = document.createElement('span');
|
||
span.textContent = 'This is a <!> that referenced this article in ';
|
||
paragraph.appendChild(span);
|
||
|
||
const link = document.createElement('a');
|
||
link.textContent = 'another thread';
|
||
span.appendChild(link);
|
||
|
||
return paragraph;
|
||
}
|
||
|
||
export function createZapthreadsContentMaybeChangedWarning() {
|
||
const paragraph = document.createElement('p');
|
||
paragraph.className = 'warning';
|
||
|
||
const span = document.createElement('span');
|
||
span.textContent = 'Article contents may have changed since this <!> was made';
|
||
paragraph.appendChild(span);
|
||
|
||
return paragraph;
|
||
}
|
||
|
||
export function createZapthreadsContentChangedWarning() {
|
||
const paragraph = document.createElement('p');
|
||
paragraph.className = 'warning';
|
||
|
||
const span = document.createElement('span');
|
||
span.textContent = 'Article contents changed since this <!> was made';
|
||
paragraph.appendChild(span);
|
||
|
||
return paragraph;
|
||
}
|
||
|
||
export function createZapthreadsCommentExpand() {
|
||
const container = document.createElement('div');
|
||
container.className = 'ztr-comment-expand';
|
||
|
||
const link = document.createElement('a');
|
||
container.appendChild(link);
|
||
|
||
const span = document.createElement('span');
|
||
span.textContent = 'Show full comment';
|
||
container.appendChild(span);
|
||
|
||
return container;
|
||
}
|
||
|
||
export function createZapthreadsRepliesContainer() {
|
||
const container = document.createElement('div');
|
||
container.className = 'ztr-comment-replies';
|
||
return container;
|
||
}
|
||
|
||
export function createZapthreadsMessageIcon() {
|
||
return createSvg(
|
||
'M 12.6030 50.4905 C 13.3758 50.4905 13.9307 50.1140 14.8621 49.2421 L 20.6483 43.8720 C 19.5188 42.9803 18.6073 41.5733 18.6073 38.3433 L 18.6073 25.2052 C 18.6073 19.1217 22.3129 15.5152 28.3766 15.5152 L 42.2479 15.5152 L 42.2281 14.7622 C 41.9306 10.6999 39.2557 8.0643 34.7177 8.0643 L 7.5301 8.0643 C 2.9922 8.0643 0 10.7791 0 15.4954 L 0 34.9548 C 0 39.6710 2.9922 42.7028 7.5301 42.7028 L 10.8195 42.7028 L 10.8195 48.4693 C 10.8195 49.6979 11.4735 50.4905 12.6030 50.4905 Z M 44.6058 53.2450 C 45.7353 53.2450 46.3895 52.4325 46.3895 51.2237 L 46.3895 45.4374 L 48.4702 45.4374 C 53.0078 45.4374 56 42.4056 56 37.7092 L 56 25.6610 C 56 20.9250 53.0078 18.2300 48.4702 18.2300 L 28.8522 18.2300 C 24.1161 18.2300 21.3221 20.9250 21.3221 25.6610 L 21.3221 37.7092 C 21.3221 42.4056 24.1161 45.4374 28.8522 45.4374 L 35.1735 45.4374 L 42.3470 51.9767 C 43.2784 52.8487 43.8331 53.2450 44.6058 53.2450 Z',
|
||
{ viewBox: '0 -6 60 60' }
|
||
);
|
||
}
|
||
|
||
export function createZapthreadsLightningIcon() {
|
||
return createSvg(
|
||
'M349.4 44.6c5.9-13.7 1.5-29.7-10.6-38.5s-28.6-8-39.9 1.8l-256 224c-10 8.8-13.6 22.9-8.9 35.3S50.7 288 64 288H175.5L98.6 467.4c-5.9 13.7-1.5 29.7 10.6 38.5s28.6 8 39.9-1.8l256-224c10-8.8 13.6-22.9 8.9-35.3s-16.6-20.7-30-20.7H272.5L349.4 44.6z',
|
||
{ viewBox: '-120 -80 528 588' }
|
||
);
|
||
}
|
||
|
||
export function createZapthreadsHeartIcon() {
|
||
return createSvg(
|
||
'M60.732 29.7C41.107 29.7 22 39.7 22 67.41c0 27.29 45.274 67.29 74 94.89 28.744-27.6 74-67.6 74-94.89 0-27.71-19.092-37.71-38.695-37.71C116 29.7 104.325 41.575 96 54.066 87.638 41.516 76 29.7 60.732 29.7z',
|
||
{ viewBox: '0 -16 180 180' }
|
||
);
|
||
}
|
||
|
||
export function createZapthreadsQuoteIcon() {
|
||
return createSvg(
|
||
'M168 80c-13.3 0-24 10.7-24 24V408c0 8.4-1.4 16.5-4.1 24H440c13.3 0 24-10.7 24-24V104c0-13.3-10.7-24-24-24H168zM72 480c-39.8 0-72-32.2-72-72V112C0 98.7 10.7 88 24 88s24 10.7 24 24V408c0 13.3 10.7 24 24 24s24-10.7 24-24V104c0-39.8 32.2-72 72-72H440c39.8 0 72 32.2 72 72V408c0 39.8-32.2 72-72 72H72zM176 136c0-13.3 10.7-24 24-24h96c13.3 0 24 10.7 24 24v80c0 13.3-10.7 24-24 24H200c-13.3 0-24-10.7-24-24V136zm200-24h32c13.3 0 24 10.7 24 24s-10.7 24-24 24H376c-13.3 0-24-10.7-24-24s10.7-24 24-24zm0 80h32c13.3 0 24 10.7 24 24s-10.7 24-24 24H376c-13.3 0-24-10.7-24-24s10.7-24 24-24zM200 272H408c13.3 0 24 10.7 24 24s-10.7 24-24 24H200c-13.3 0-24-10.7-24-24s10.7-24 24-24zm0 80H408c13.3 0 24 10.7 24 24s-10.7 24-24 24H200c-13.3 0-24-10.7-24-24s10.7-24 24-24z',
|
||
{ viewBox: '0 0 576 512' }
|
||
);
|
||
}
|
||
|
||
export function createZapthreadsEllipsisIcon() {
|
||
return createSvg(
|
||
'M8 256a56 56 0 1 1 112 0A56 56 0 1 1 8 256zm160 0a56 56 0 1 1 112 0 56 56 0 1 1 -112 0zm216-56a56 56 0 1 1 0 112 56 56 0 1 1 0-112z',
|
||
{ viewBox: '0 -200 560 640' }
|
||
);
|
||
}
|
||
|
||
export function createZapthreadsPlayIcon() {
|
||
return createSvg(
|
||
'M246.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 256c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l128-128z',
|
||
{ viewBox: '0 -50 256 512' }
|
||
);
|
||
}
|
||
|
||
export function createZapthreadsReplyIcon() {
|
||
return createSvg(
|
||
'M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z',
|
||
{ viewBox: '0 -50 320 512' }
|
||
);
|
||
}
|
||
|
||
export function createZapthreadsAlertIcon() {
|
||
const svg = createSvg(
|
||
'M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z',
|
||
{ viewBox: '0 0 512 512' }
|
||
);
|
||
return svg;
|
||
}
|
||
|
||
const ZAPTHREADS_MOUNT_CLASS = 'blog-ztr-container';
|
||
|
||
function removeExistingZapthreadsMount(blogRoot) {
|
||
const existingRoot = blogRoot.querySelector('#ztr-root');
|
||
if (!existingRoot) {
|
||
return;
|
||
}
|
||
|
||
const mountContainer = existingRoot.closest(`.${ZAPTHREADS_MOUNT_CLASS}`);
|
||
if (mountContainer && mountContainer.parentNode) {
|
||
mountContainer.parentNode.removeChild(mountContainer);
|
||
return;
|
||
}
|
||
|
||
existingRoot.remove();
|
||
}
|
||
|
||
function createZapthreadsMountContainer(blogRoot) {
|
||
const container = document.createElement('div');
|
||
container.className = ZAPTHREADS_MOUNT_CLASS;
|
||
blogRoot.appendChild(container);
|
||
return container;
|
||
}
|
||
|
||
export function createZapthreadsRoot() {
|
||
const root = document.createElement('div');
|
||
root.id = 'ztr-root';
|
||
|
||
const watermark = document.createElement('div');
|
||
watermark.className = 'blog-ztr-watermark';
|
||
root.appendChild(watermark);
|
||
|
||
return root;
|
||
}
|
||
|
||
export function createZapthreadsContent() {
|
||
const content = document.createElement('div');
|
||
content.id = 'ztr-content';
|
||
return content;
|
||
}
|
||
|
||
export function createZapthreadsErrorHeading() {
|
||
const heading = document.createElement('h1');
|
||
heading.textContent = 'Error!';
|
||
return heading;
|
||
}
|
||
|
||
export function createZapthreadsErrorDetails() {
|
||
const container = document.createElement('div');
|
||
container.className = 'ztr-comment-text';
|
||
|
||
const pre = document.createElement('pre');
|
||
container.appendChild(pre);
|
||
|
||
const message = document.createElement('p');
|
||
message.textContent =
|
||
'Only properly formed NIP-19 naddr, note and nevent encoded entities and URLs are supported.';
|
||
container.appendChild(message);
|
||
|
||
return container;
|
||
}
|
||
|
||
export function createZapthreadsTitleHeading() {
|
||
const heading = document.createElement('h2');
|
||
heading.id = 'ztr-title';
|
||
return heading;
|
||
}
|
||
|
||
export const DEFAULT_THREAD_TITLE = 'Thread';
|
||
|
||
export function renderThreadTitle(titleEl, title = DEFAULT_THREAD_TITLE) {
|
||
if (!titleEl) return;
|
||
titleEl.innerHTML = escapeHTML(title || DEFAULT_THREAD_TITLE);
|
||
}
|
||
|
||
export function createZapthreadsPoweredByMessage() {
|
||
const paragraph = document.createElement('p');
|
||
paragraph.appendChild(document.createTextNode('Powered by '));
|
||
|
||
const link = document.createElement('a');
|
||
link.href = 'https://github.com/fr4nzap/zapthreads';
|
||
link.textContent = 'zapthreads';
|
||
paragraph.appendChild(link);
|
||
|
||
return paragraph;
|
||
}
|
||
|
||
export function createZapthreadsClearCacheButton() {
|
||
const button = document.createElement('button');
|
||
button.textContent = 'Clear cache';
|
||
return button;
|
||
}
|
||
|
||
export function createZapthreadsAnchorVersionLabel() {
|
||
const paragraph = document.createElement('p');
|
||
paragraph.textContent = 'Anchor version: ';
|
||
return paragraph;
|
||
}
|
||
|
||
export const DEFAULT_RELAYS = [
|
||
'wss://relay.damus.io',
|
||
'wss://nos.lol',
|
||
];
|
||
|
||
export const DEFAULT_DISABLE_FEATURES = [];
|
||
|
||
export function parseRelayList(relayList = '') {
|
||
if (!relayList.trim()) {
|
||
return [...DEFAULT_RELAYS];
|
||
}
|
||
|
||
const relays = [];
|
||
|
||
for (const item of relayList.split(',')) {
|
||
const trimmed = item.trim();
|
||
if (!trimmed) continue;
|
||
|
||
try {
|
||
relays.push(new URL(trimmed).toString());
|
||
} catch (error) {
|
||
console.warn('[zapthreads] ignored invalid relay input', trimmed, error);
|
||
}
|
||
}
|
||
|
||
return relays.length > 0 ? relays : [...DEFAULT_RELAYS];
|
||
}
|
||
|
||
export function parseDisableList(disableList = '') {
|
||
return disableList
|
||
.split(',')
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
export function parseUrlOptions(urls = '') {
|
||
if (!urls || typeof urls !== 'string') {
|
||
return {};
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(urls);
|
||
if (parsed && typeof parsed === 'object') {
|
||
return parsed;
|
||
}
|
||
} catch (error) {
|
||
// Ignore JSON parse errors and fall back to manual parsing below.
|
||
}
|
||
|
||
const map = {};
|
||
for (const segment of urls.split(',')) {
|
||
const [rawKey, rawValue] = segment.split('=');
|
||
if (!rawKey || !rawValue) continue;
|
||
const key = rawKey.trim();
|
||
const value = rawValue.trim();
|
||
if (!key || !value) continue;
|
||
map[key] = value;
|
||
}
|
||
|
||
return map;
|
||
}
|
||
|
||
export function normalizeAnchor(anchor, { legacyUrl = false } = {}) {
|
||
const trimmed = (anchor || '').trim();
|
||
|
||
if (!trimmed) {
|
||
return { type: 'error', value: 'Missing anchor value' };
|
||
}
|
||
|
||
try {
|
||
if (trimmed.startsWith('http')) {
|
||
const normalized = legacyUrl ? trimmed : normalizeHttpAnchor(trimmed);
|
||
return { type: 'http', value: normalized };
|
||
}
|
||
|
||
const decoded = decodeNostrEntity(trimmed);
|
||
|
||
if (!decoded) {
|
||
return { type: 'error', value: `Unsupported anchor: ${trimmed}` };
|
||
}
|
||
|
||
if (decoded.type === 'note' || decoded.type === 'nevent') {
|
||
return { type: 'note', value: decoded.id };
|
||
}
|
||
|
||
if (decoded.type === 'naddr') {
|
||
const { kind, pubkey, identifier } = decoded;
|
||
return { type: 'naddr', value: `${kind}:${pubkey}:${identifier}` };
|
||
}
|
||
|
||
return { type: 'error', value: `Unsupported anchor type: ${decoded.type}` };
|
||
} catch (error) {
|
||
return { type: 'error', value: `Malformed anchor: ${trimmed}`, error };
|
||
}
|
||
}
|
||
|
||
export function createState(options = {}) {
|
||
const {
|
||
anchor = '',
|
||
legacyUrl = false,
|
||
relays = '',
|
||
disable = '',
|
||
version = '',
|
||
} = options;
|
||
|
||
const normalizedAnchor = normalizeAnchor(anchor, { legacyUrl });
|
||
|
||
return {
|
||
anchor: normalizedAnchor,
|
||
version,
|
||
relays: parseRelayList(relays),
|
||
disableFeatures: parseDisableList(disable),
|
||
legacyUrl,
|
||
filter: {},
|
||
rootEventIds: [],
|
||
anchorAuthor: undefined,
|
||
subscription: null,
|
||
eventMap: new Map(),
|
||
events: [],
|
||
};
|
||
}
|
||
|
||
export function updateFilterForAnchor(state) {
|
||
if (!state || !state.anchor) {
|
||
return;
|
||
}
|
||
|
||
const { anchor, rootEventIds, version } = state;
|
||
|
||
if (anchor.type === 'http' || anchor.type === 'note') {
|
||
if (!rootEventIds.length) {
|
||
state.filter = {};
|
||
return;
|
||
}
|
||
|
||
state.filter = { '#e': [...rootEventIds] };
|
||
return;
|
||
}
|
||
|
||
if (anchor.type === 'naddr') {
|
||
state.filter = { '#a': [anchor.value] };
|
||
state.version = version || rootEventIds[0] || '';
|
||
}
|
||
}
|
||
|
||
export function updateStateFromEvents(
|
||
state,
|
||
events = [],
|
||
{ parseEvent = Na } = {}
|
||
) {
|
||
if (!state || !Array.isArray(events) || events.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const parsedEvents = [];
|
||
const rootIds = new Set(state.rootEventIds);
|
||
const parser = typeof parseEvent === 'function' ? parseEvent : null;
|
||
|
||
if (!state.eventMap || typeof state.eventMap.set !== 'function') {
|
||
state.eventMap = new Map();
|
||
}
|
||
|
||
for (const event of events) {
|
||
if (!event) continue;
|
||
const parsed = parser ? parser(event) : event;
|
||
if (!parsed) continue;
|
||
|
||
parsedEvents.push(parsed);
|
||
|
||
const rootId = parsed.ro || parsed.id || event.id;
|
||
if (rootId) {
|
||
rootIds.add(rootId);
|
||
}
|
||
|
||
if (
|
||
!state.anchorAuthor &&
|
||
(!parsed.ro || parsed.ro === parsed.id) &&
|
||
parsed.pk
|
||
) {
|
||
state.anchorAuthor = parsed.pk;
|
||
}
|
||
|
||
const eventId = parsed.id || event.id;
|
||
if (!eventId) {
|
||
continue;
|
||
}
|
||
|
||
const previous = state.eventMap.get(eventId) || {};
|
||
state.eventMap.set(eventId, {
|
||
...previous,
|
||
...parsed,
|
||
});
|
||
}
|
||
|
||
if (rootIds.size > 0) {
|
||
state.rootEventIds = [...rootIds];
|
||
}
|
||
|
||
if (!state.version && state.rootEventIds.length > 0) {
|
||
state.version = state.rootEventIds[0];
|
||
}
|
||
|
||
updateFilterForAnchor(state);
|
||
|
||
state.events = Array.from(state.eventMap.values()).sort((a, b) => {
|
||
const aTs = typeof a.ts === 'number' ? a.ts : 0;
|
||
const bTs = typeof b.ts === 'number' ? b.ts : 0;
|
||
return bTs - aTs;
|
||
});
|
||
|
||
return parsedEvents;
|
||
}
|
||
|
||
function normalizeBaseUrl(baseUrl) {
|
||
if (!baseUrl) {
|
||
return 'https://njump.me/';
|
||
}
|
||
return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
||
}
|
||
|
||
function createProfileUrl(pubkey, baseUrl) {
|
||
if (!pubkey) {
|
||
return '#';
|
||
}
|
||
const normalized = normalizeBaseUrl(baseUrl);
|
||
return `${normalized}${pubkey}`;
|
||
}
|
||
|
||
function createEventUrl(eventId, baseUrl) {
|
||
if (!eventId) {
|
||
return '#';
|
||
}
|
||
const normalized = normalizeBaseUrl(baseUrl);
|
||
return `${normalized}${eventId}`;
|
||
}
|
||
|
||
export function formatEventTimestamp(timestamp) {
|
||
if (!timestamp && timestamp !== 0) {
|
||
return '';
|
||
}
|
||
|
||
const date = new Date(timestamp * 1000);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
return new Intl.DateTimeFormat(undefined, {
|
||
dateStyle: 'medium',
|
||
timeStyle: 'short',
|
||
}).format(date);
|
||
} catch (error) {
|
||
return date.toISOString();
|
||
}
|
||
}
|
||
|
||
export function formatAuthorName(pubkey) {
|
||
if (!pubkey) {
|
||
return 'Unknown';
|
||
}
|
||
|
||
const trimmed = pubkey.trim();
|
||
if (trimmed.length <= 16) {
|
||
return trimmed;
|
||
}
|
||
|
||
return `${trimmed.slice(0, 8)}…${trimmed.slice(-8)}`;
|
||
}
|
||
|
||
function truncateContent(content = '', limit = 280) {
|
||
const text = content.trim();
|
||
if (text.length <= limit) {
|
||
return text;
|
||
}
|
||
return `${text.slice(0, Math.max(0, limit - 1))}…`;
|
||
}
|
||
|
||
function convertContentToHtml(content = '') {
|
||
return escapeHTML(content).replace(/\n/g, '<br />');
|
||
}
|
||
|
||
function isFeatureDisabled(feature, disabled = []) {
|
||
return Array.isArray(disabled) && disabled.includes(feature);
|
||
}
|
||
|
||
export function renderThread(
|
||
threadEl,
|
||
events = [],
|
||
{
|
||
anchor,
|
||
disableFeatures = [],
|
||
urlOptions = {},
|
||
} = {},
|
||
) {
|
||
if (!threadEl) {
|
||
return;
|
||
}
|
||
|
||
clearElement(threadEl);
|
||
|
||
const list = Array.isArray(events) ? [...events] : [];
|
||
if (list.length === 0) {
|
||
const empty = document.createElement('p');
|
||
empty.className = 'ztr-thread-empty';
|
||
empty.textContent = 'No comments yet.';
|
||
threadEl.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
const profileBaseUrl = normalizeBaseUrl(
|
||
urlOptions.profile || urlOptions.profileBase || 'https://njump.me/',
|
||
);
|
||
const eventBaseUrl = normalizeBaseUrl(
|
||
urlOptions.event || urlOptions.eventBase || urlOptions.profile || 'https://njump.me/',
|
||
);
|
||
|
||
list.sort((a, b) => {
|
||
const aTs = typeof a.ts === 'number' ? a.ts : 0;
|
||
const bTs = typeof b.ts === 'number' ? b.ts : 0;
|
||
return bTs - aTs;
|
||
});
|
||
|
||
for (const event of list) {
|
||
const comment = createZapthreadsCommentContainer();
|
||
const pictureWrapper = comment.querySelector('.ztr-comment-info-picture');
|
||
const pictureImage = pictureWrapper ? pictureWrapper.querySelector('img') : null;
|
||
if (pictureImage) {
|
||
pictureImage.alt = 'User avatar';
|
||
pictureImage.decoding = 'async';
|
||
pictureImage.loading = 'lazy';
|
||
if (event.picture) {
|
||
pictureImage.src = event.picture;
|
||
} else {
|
||
pictureImage.removeAttribute('src');
|
||
}
|
||
}
|
||
|
||
const authorLink = comment.querySelector('.ztr-comment-info-author a');
|
||
if (authorLink) {
|
||
authorLink.href = createProfileUrl(event.pk, profileBaseUrl);
|
||
authorLink.textContent = formatAuthorName(event.pk);
|
||
authorLink.rel = 'noopener';
|
||
authorLink.target = '_blank';
|
||
}
|
||
|
||
const authorBadge = comment.querySelector('.ztr-comment-info-author strong');
|
||
if (authorBadge) {
|
||
authorBadge.textContent = ' posted';
|
||
}
|
||
|
||
const dotsLink = comment.querySelector('.ztr-comment-info-dots');
|
||
if (dotsLink) {
|
||
dotsLink.href = createEventUrl(event.id, eventBaseUrl);
|
||
dotsLink.textContent = 'View';
|
||
dotsLink.target = '_blank';
|
||
dotsLink.rel = 'noopener';
|
||
}
|
||
|
||
const infoLists = comment.querySelectorAll('.ztr-comment-info-items');
|
||
const metaList = infoLists[1];
|
||
if (metaList) {
|
||
clearElement(metaList);
|
||
const timestampItem = document.createElement('li');
|
||
timestampItem.textContent = formatEventTimestamp(event.ts);
|
||
metaList.appendChild(timestampItem);
|
||
|
||
if (event.ro && anchor && anchor.value && event.ro !== anchor.value) {
|
||
const bullet = createZapthreadsBulletSeparator();
|
||
metaList.appendChild(bullet);
|
||
const crossPost = createZapthreadsRepliesLabel();
|
||
crossPost.textContent = 'Cross-post';
|
||
metaList.appendChild(crossPost);
|
||
}
|
||
}
|
||
|
||
const [truncatedText, fullText] = comment.querySelectorAll('.ztr-comment-text');
|
||
if (truncatedText) {
|
||
truncatedText.innerHTML = convertContentToHtml(truncateContent(event.c));
|
||
}
|
||
if (fullText) {
|
||
fullText.innerHTML = convertContentToHtml(event.c);
|
||
}
|
||
|
||
const actions = comment.querySelector('.ztr-comment-actions');
|
||
if (actions) {
|
||
clearElement(actions);
|
||
|
||
if (!isFeatureDisabled('reply', disableFeatures)) {
|
||
const replyAction = createZapthreadsReplyActionItem();
|
||
const replySpan = replyAction.querySelector('span');
|
||
if (replySpan) {
|
||
replySpan.textContent = 'Reply';
|
||
}
|
||
actions.appendChild(replyAction);
|
||
}
|
||
|
||
if (!isFeatureDisabled('likes', disableFeatures)) {
|
||
const likeAction = createZapthreadsLikeAction();
|
||
const likeSpan = likeAction.querySelector('span');
|
||
if (likeSpan) {
|
||
const count = typeof event.likeCount === 'number' ? event.likeCount : 0;
|
||
likeSpan.textContent = `${count} likes`;
|
||
}
|
||
actions.appendChild(likeAction);
|
||
}
|
||
|
||
if (!isFeatureDisabled('zaps', disableFeatures)) {
|
||
const zapAction = createZapthreadsZapAction();
|
||
const zapSpan = zapAction.querySelector('span');
|
||
if (zapSpan) {
|
||
const total = typeof event.zapTotal === 'number' ? event.zapTotal : 0;
|
||
zapSpan.textContent = `${total} sats`;
|
||
}
|
||
actions.appendChild(zapAction);
|
||
}
|
||
}
|
||
|
||
const infoPane = createZapthreadsInfoPane();
|
||
const infoLink = infoPane.querySelector('a');
|
||
if (infoLink) {
|
||
infoLink.href = createEventUrl(event.id, eventBaseUrl);
|
||
infoLink.rel = 'noopener';
|
||
infoLink.target = '_blank';
|
||
}
|
||
comment.appendChild(infoPane);
|
||
|
||
threadEl.appendChild(comment);
|
||
}
|
||
}
|
||
|
||
export async function loadRootEvents({
|
||
state,
|
||
data,
|
||
onCache,
|
||
onNetwork,
|
||
}) {
|
||
if (!state || !data) return { cache: [], network: [] };
|
||
|
||
const { anchor, relays } = state;
|
||
|
||
if (!anchor || anchor.type === 'error') {
|
||
return { cache: [], network: [] };
|
||
}
|
||
|
||
const cacheResults = [];
|
||
const networkResults = [];
|
||
|
||
if (anchor.type === 'http') {
|
||
const cached = await data.Ti('events', anchor.value, { index: 'r' });
|
||
cacheResults.push(...cached);
|
||
} else if (anchor.type === 'note') {
|
||
const cached = await data.Ti('events', anchor.value, { index: 'id' });
|
||
cacheResults.push(...cached);
|
||
} else if (anchor.type === 'naddr') {
|
||
const cached = await data.Ti('events', anchor.value, { index: 'a' });
|
||
cacheResults.push(...cached);
|
||
}
|
||
|
||
if (onCache) {
|
||
onCache(cacheResults);
|
||
}
|
||
|
||
if (data.di && typeof data.di.querySync === 'function') {
|
||
const filter = createFilterForAnchor(state);
|
||
const queryResults = await data.di.querySync(relays, filter);
|
||
networkResults.push(...queryResults);
|
||
if (onNetwork) {
|
||
onNetwork(queryResults);
|
||
}
|
||
}
|
||
|
||
return { cache: cacheResults, network: networkResults };
|
||
}
|
||
|
||
export function createFilterForAnchor(state) {
|
||
const { anchor, rootEventIds, legacyUrl } = state;
|
||
|
||
if (!anchor) return {};
|
||
|
||
if (anchor.type === 'http') {
|
||
const values = [anchor.value];
|
||
if (!legacyUrl) {
|
||
values.push(`${anchor.value}/`);
|
||
}
|
||
return { '#r': values, kinds: [1, 8812] };
|
||
}
|
||
|
||
if (anchor.type === 'note') {
|
||
return { ids: [anchor.value] };
|
||
}
|
||
|
||
if (anchor.type === 'naddr') {
|
||
return { '#a': [anchor.value] };
|
||
}
|
||
|
||
return {};
|
||
}
|
||
|
||
export function decodeNostrEntity(entity) {
|
||
try {
|
||
const { decode } = window.NostrTools?.nip19 ?? {};
|
||
if (!decode) return null;
|
||
const result = decode(entity);
|
||
if (!result) return null;
|
||
|
||
switch (result.type) {
|
||
case 'nevent':
|
||
return { type: 'note', id: result.data.id };
|
||
case 'note':
|
||
return { type: 'note', id: result.data };
|
||
case 'naddr': {
|
||
const { kind, pubkey, identifier } = result.data;
|
||
return { type: 'naddr', kind, pubkey, identifier };
|
||
}
|
||
default:
|
||
return { type: result.type };
|
||
}
|
||
} catch (error) {
|
||
return { type: 'error', error };
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
export function normalizeHttpAnchor(url) {
|
||
try {
|
||
const normalized = new URL(url);
|
||
normalized.hash = '';
|
||
return normalized.toString().replace(/\/$/, '');
|
||
} catch (error) {
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export function renderWatermark(watermarkEl) {
|
||
if (!watermarkEl) return;
|
||
watermarkEl.textContent = '';
|
||
const icon = createZapthreadsEllipsisIcon();
|
||
icon.classList.add('ztr-watermark-icon');
|
||
watermarkEl.appendChild(icon);
|
||
const label = document.createElement('span');
|
||
label.textContent = ' Zapthreads';
|
||
watermarkEl.appendChild(label);
|
||
}
|
||
|
||
export function renderAnchorVersion(labelEl, version) {
|
||
if (!labelEl) return;
|
||
labelEl.innerHTML = `Anchor version: ${escapeHTML(version || 'unknown')}`;
|
||
}
|
||
|
||
export const ZAPTHREADS_EVENT_MOUNTED = 'zapthreads:mounted';
|
||
export const ZAPTHREADS_EVENT_UNMOUNTED = 'zapthreads:unmounted';
|
||
export const ZAPTHREADS_EVENT_CLEAR_CACHE = 'zapthreads:clear-cache';
|
||
|
||
export function renderClearCache(buttonEl, clearCache) {
|
||
if (!buttonEl) return () => {};
|
||
const handler = () => {
|
||
if (typeof clearCache === 'function') {
|
||
clearCache();
|
||
}
|
||
buttonEl.dispatchEvent(
|
||
new CustomEvent(ZAPTHREADS_EVENT_CLEAR_CACHE, {
|
||
bubbles: true,
|
||
detail: { source: 'button' },
|
||
})
|
||
);
|
||
};
|
||
buttonEl.addEventListener('click', handler);
|
||
return () => {
|
||
buttonEl.removeEventListener('click', handler);
|
||
};
|
||
}
|
||
|
||
export function clearElement(element) {
|
||
if (!element) return;
|
||
while (element.firstChild) {
|
||
element.removeChild(element.firstChild);
|
||
}
|
||
}
|
||
|
||
function getBlogAppRoot() {
|
||
return document.querySelector('.blog-app-root');
|
||
}
|
||
|
||
function dispatchZapthreadsEvent(targets, type, detail) {
|
||
for (const target of targets) {
|
||
if (!target || typeof target.dispatchEvent !== 'function') continue;
|
||
target.dispatchEvent(
|
||
new CustomEvent(type, {
|
||
bubbles: true,
|
||
detail: detail ? { ...detail } : {},
|
||
})
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function initZapthreadsEmbed({
|
||
root,
|
||
options,
|
||
dataUtils,
|
||
} = {}) {
|
||
if (!root) {
|
||
throw new Error('initZapthreadsEmbed requires a root element');
|
||
}
|
||
|
||
const data = dataUtils || {};
|
||
const state = createState(options);
|
||
const urlOptions = parseUrlOptions(
|
||
options && typeof options.urls === 'string' ? options.urls : '',
|
||
);
|
||
|
||
const blogRoot = getBlogAppRoot();
|
||
if (!blogRoot) {
|
||
throw new Error('initZapthreadsEmbed requires .blog-app-root container');
|
||
}
|
||
|
||
const rootEl = createZapthreadsRoot();
|
||
|
||
const watermarkEl = rootEl.querySelector('.blog-ztr-watermark');
|
||
renderWatermark(watermarkEl);
|
||
|
||
const contentEl = createZapthreadsContent();
|
||
rootEl.appendChild(contentEl);
|
||
|
||
const titleEl = createZapthreadsTitleHeading();
|
||
contentEl.appendChild(titleEl);
|
||
let currentTitle = DEFAULT_THREAD_TITLE;
|
||
let hasCustomTitle = false;
|
||
renderThreadTitle(titleEl, currentTitle);
|
||
|
||
const poweredByEl = createZapthreadsPoweredByMessage();
|
||
contentEl.appendChild(poweredByEl);
|
||
|
||
const anchorVersionEl = createZapthreadsAnchorVersionLabel();
|
||
contentEl.appendChild(anchorVersionEl);
|
||
renderAnchorVersion(anchorVersionEl, state.version);
|
||
|
||
const clearCacheButton = createZapthreadsClearCacheButton();
|
||
contentEl.appendChild(clearCacheButton);
|
||
|
||
const threadEl = createZapthreadsThreadContainer();
|
||
contentEl.appendChild(threadEl);
|
||
renderThread(threadEl, state.events, {
|
||
anchor: state.anchor,
|
||
disableFeatures: state.disableFeatures,
|
||
urlOptions,
|
||
});
|
||
|
||
const disposeCallbacks = new Set();
|
||
const registerDispose = (callback) => {
|
||
if (typeof callback === 'function') {
|
||
disposeCallbacks.add(callback);
|
||
}
|
||
};
|
||
|
||
const clearCacheCleanup = renderClearCache(clearCacheButton, data.clearCache);
|
||
registerDispose(clearCacheCleanup);
|
||
|
||
removeExistingZapthreadsMount(blogRoot);
|
||
const mountContainer = createZapthreadsMountContainer(blogRoot);
|
||
mountContainer.appendChild(rootEl);
|
||
|
||
const lifecycleTargets = [rootEl];
|
||
if (root && root !== rootEl && typeof root.dispatchEvent === 'function') {
|
||
lifecycleTargets.push(root);
|
||
}
|
||
|
||
const lifecycleDetail = () => ({ anchor: state.anchor, root: rootEl });
|
||
|
||
const renderError = (message) => {
|
||
clearElement(contentEl);
|
||
const heading = createZapthreadsErrorHeading();
|
||
heading.textContent = 'Error!';
|
||
const details = createZapthreadsErrorDetails();
|
||
const pre = details.querySelector('pre');
|
||
if (pre) {
|
||
pre.innerHTML = escapeHTML(message || '');
|
||
}
|
||
contentEl.appendChild(heading);
|
||
contentEl.appendChild(details);
|
||
};
|
||
|
||
if (state.anchor.type === 'error') {
|
||
renderError(state.anchor.value);
|
||
dispatchZapthreadsEvent(lifecycleTargets, ZAPTHREADS_EVENT_MOUNTED, lifecycleDetail());
|
||
return () => {
|
||
disposeCallbacks.forEach((callback) => {
|
||
try {
|
||
callback();
|
||
} catch (error) {
|
||
console.error('[zapthreads] cleanup failed', error);
|
||
}
|
||
});
|
||
disposeCallbacks.clear();
|
||
dispatchZapthreadsEvent(
|
||
lifecycleTargets,
|
||
ZAPTHREADS_EVENT_UNMOUNTED,
|
||
lifecycleDetail()
|
||
);
|
||
if (mountContainer.parentNode) {
|
||
mountContainer.parentNode.removeChild(mountContainer);
|
||
}
|
||
};
|
||
}
|
||
|
||
dispatchZapthreadsEvent(
|
||
lifecycleTargets,
|
||
ZAPTHREADS_EVENT_MOUNTED,
|
||
lifecycleDetail()
|
||
);
|
||
|
||
updateFilterForAnchor(state);
|
||
renderAnchorVersion(anchorVersionEl, state.version);
|
||
|
||
const parseEvent = typeof data.Na === 'function' ? data.Na : null;
|
||
|
||
let currentSubscription = null;
|
||
let currentFilterKey = null;
|
||
const closeSubscription = () => {
|
||
if (currentSubscription && typeof currentSubscription.close === 'function') {
|
||
try {
|
||
currentSubscription.close();
|
||
} catch (error) {
|
||
console.error('[zapthreads] failed to close subscription', error);
|
||
}
|
||
}
|
||
currentSubscription = null;
|
||
currentFilterKey = null;
|
||
state.subscription = null;
|
||
};
|
||
registerDispose(closeSubscription);
|
||
|
||
const processEvents = (events = []) => {
|
||
const parsedEvents = updateStateFromEvents(state, events, { parseEvent });
|
||
if (parsedEvents.length > 0) {
|
||
const eventWithTitle = parsedEvents.find((item) => item && item.tl);
|
||
if (eventWithTitle) {
|
||
hasCustomTitle = true;
|
||
currentTitle = eventWithTitle.tl || DEFAULT_THREAD_TITLE;
|
||
renderThreadTitle(titleEl, currentTitle);
|
||
}
|
||
} else if (!hasCustomTitle) {
|
||
renderThreadTitle(titleEl, currentTitle);
|
||
}
|
||
renderAnchorVersion(anchorVersionEl, state.version);
|
||
renderThread(threadEl, state.events, {
|
||
anchor: state.anchor,
|
||
disableFeatures: state.disableFeatures,
|
||
urlOptions,
|
||
});
|
||
return parsedEvents;
|
||
};
|
||
|
||
const ensureSubscription = () => {
|
||
if (!data.di || typeof data.di.subscribeMany !== 'function') return;
|
||
if (!state.filter || Object.keys(state.filter).length === 0) return;
|
||
|
||
const nextKey = JSON.stringify(state.filter);
|
||
if (currentSubscription && currentFilterKey === nextKey) {
|
||
return;
|
||
}
|
||
|
||
closeSubscription();
|
||
currentFilterKey = nextKey;
|
||
currentSubscription = data.di.subscribeMany(state.relays, [state.filter], {
|
||
onevent(event) {
|
||
if (!event) return;
|
||
processEvents([event]);
|
||
if (typeof data.un === 'function') {
|
||
data.un('events', event, { immediate: true });
|
||
}
|
||
},
|
||
});
|
||
state.subscription = currentSubscription;
|
||
};
|
||
|
||
await loadRootEvents({
|
||
state,
|
||
data,
|
||
onCache(events = []) {
|
||
processEvents(events);
|
||
ensureSubscription();
|
||
},
|
||
onNetwork(events = []) {
|
||
if (!Array.isArray(events) || events.length === 0) {
|
||
ensureSubscription();
|
||
return;
|
||
}
|
||
processEvents(events);
|
||
for (const event of events) {
|
||
if (event && typeof data.un === 'function') {
|
||
data.un('events', event, { immediate: true });
|
||
}
|
||
}
|
||
ensureSubscription();
|
||
},
|
||
});
|
||
|
||
ensureSubscription();
|
||
|
||
return () => {
|
||
disposeCallbacks.forEach((callback) => {
|
||
try {
|
||
callback();
|
||
} catch (error) {
|
||
console.error('[zapthreads] cleanup failed', error);
|
||
}
|
||
});
|
||
disposeCallbacks.clear();
|
||
dispatchZapthreadsEvent(
|
||
lifecycleTargets,
|
||
ZAPTHREADS_EVENT_UNMOUNTED,
|
||
lifecycleDetail()
|
||
);
|
||
if (mountContainer.parentNode) {
|
||
mountContainer.parentNode.removeChild(mountContainer);
|
||
}
|
||
};
|
||
}
|