Merge pull request #487 from PR0M3TH3AN/codex/refactor-zapthreads-code-into-new-module

Refine zapthreads embed rendering
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-13 13:15:58 -04:00
commit 23652c9722
1 file changed
+283
+283
View File
@@ -482,6 +482,33 @@ export function parseDisableList(disableList = '') {
.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();
@@ -537,6 +564,8 @@ export function createState(options = {}) {
rootEventIds: [],
anchorAuthor: undefined,
subscription: null,
eventMap: new Map(),
events: [],
};
}
@@ -576,6 +605,10 @@ export function updateStateFromEvents(
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;
@@ -595,6 +628,17 @@ export function updateStateFromEvents(
) {
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) {
@@ -607,9 +651,232 @@ export function updateStateFromEvents(
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,
@@ -775,6 +1042,9 @@ export async function initZapthreadsEmbed({
const data = dataUtils || {};
const state = createState(options);
const urlOptions = parseUrlOptions(
options && typeof options.urls === 'string' ? options.urls : '',
);
const blogRoot = getBlogAppRoot();
if (!blogRoot) {
@@ -810,6 +1080,14 @@ export async function initZapthreadsEmbed({
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') {
@@ -906,6 +1184,11 @@ export async function initZapthreadsEmbed({
renderThreadTitle(titleEl, currentTitle);
}
renderAnchorVersion(anchorVersionEl, state.version);
renderThread(threadEl, state.events, {
anchor: state.anchor,
disableFeatures: state.disableFeatures,
urlOptions,
});
return parsedEvents;
};