mirror of
https://github.com/svemagie/blog-eleventy-indiekit.git
synced 2026-05-14 22:48:50 +02:00
refactor: unified owner reply threading via conversations API
- Remove self-mention filter (siteOrigin, isSelfMention) from webmentions.js - Remove build-time self-mention filter from eleventy.config.js - processWebmentions() now separates is_owner items and threads them under parent interaction cards via threadOwnerReplies() - owner:detected handler reduced to wireReplyButtons() only - Remove loadOwnerReplies() and Alpine.store replies from comments.js - Owner replies now come from conversations API with parent_url metadata Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
This commit is contained in:
@@ -509,6 +509,20 @@ export default async function () {
|
|||||||
3. **CSS utilities:** Add custom utilities to `css/tailwind.css`
|
3. **CSS utilities:** Add custom utilities to `css/tailwind.css`
|
||||||
4. **Rebuild CSS:** `npm run build:css` (or `make build:css` in parent repo)
|
4. **Rebuild CSS:** `npm run build:css` (or `make build:css` in parent repo)
|
||||||
|
|
||||||
|
## Performance Debugging
|
||||||
|
|
||||||
|
For diagnosing and fixing Eleventy build performance issues, see the comprehensive guide at `/home/rick/code/indiekit-dev/docs/eleventy-debugging-guide.md`.
|
||||||
|
|
||||||
|
**Quick diagnostic steps:**
|
||||||
|
|
||||||
|
1. **Baseline:** `time npx @11ty/eleventy --quiet` (run 3x, take median)
|
||||||
|
2. **Benchmark:** `DEBUG=Eleventy:Benchmark* npx @11ty/eleventy` — find entries >15% of total or with call count matching page count
|
||||||
|
3. **Classify:** Network requests (high avg, low count) vs. redundant computation (low avg, high count) vs. client-side bloat (fast build, low Lighthouse)
|
||||||
|
4. **Fix:** Timeout + cache for network; memoize with `Map` for per-page computation; Web Components for client-side bloat
|
||||||
|
5. **Verify:** Re-measure against baseline
|
||||||
|
|
||||||
|
**Relevant to this theme:** Data files in `_data/` that fetch from external APIs (GitHub, Mastodon, Bluesky, YouTube, Funkwhale, Last.fm) are Pattern A candidates — always use `eleventy-fetch` with appropriate `duration` and handle failures gracefully. The OG image generation hook is a Pattern B candidate — it already uses batch spawning and manifest caching to manage memory and avoid redundant work.
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
1. ❌ **Forgetting to update submodule** after changes
|
1. ❌ **Forgetting to update submodule** after changes
|
||||||
|
|||||||
+2
-8
@@ -820,14 +820,8 @@ export default function (eleventyConfig) {
|
|||||||
urlsToCheck.add(`${siteUrl}${contentUrl}`.replace(/\/$/, ""));
|
urlsToCheck.add(`${siteUrl}${contentUrl}`.replace(/\/$/, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter merged data matching any of our URLs, excluding self-mentions
|
// Filter merged data matching any of our URLs
|
||||||
// (owner replies sent via webmention-sender appear as webmentions on own posts)
|
const matched = merged.filter((wm) => urlsToCheck.has(wm["wm-target"]));
|
||||||
const matched = merged.filter((wm) => {
|
|
||||||
if (!urlsToCheck.has(wm["wm-target"])) return false;
|
|
||||||
const source = wm["wm-source"] || wm.url || "";
|
|
||||||
if (source.startsWith(siteUrl)) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Deduplicate cross-source: same author + same interaction type = same mention
|
// Deduplicate cross-source: same author + same interaction type = same mention
|
||||||
// (webmention.io and conversations API may both report the same like/reply)
|
// (webmention.io and conversations API may both report the same like/reply)
|
||||||
|
|||||||
+3
-20
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Client-side comments component (Alpine.js)
|
* Client-side comments component (Alpine.js)
|
||||||
* Handles IndieAuth flow, comment submission, display, and owner replies
|
* Handles IndieAuth flow, comment submission, display, and owner detection
|
||||||
*
|
*
|
||||||
* Registered via Alpine.data() so the component is available
|
* Registered via Alpine.data() so the component is available
|
||||||
* regardless of script loading order.
|
* regardless of script loading order.
|
||||||
@@ -12,7 +12,6 @@ document.addEventListener("alpine:init", () => {
|
|||||||
isOwner: false,
|
isOwner: false,
|
||||||
profile: null,
|
profile: null,
|
||||||
syndicationTargets: {},
|
syndicationTargets: {},
|
||||||
replies: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Alpine.data("commentsSection", (targetUrl) => ({
|
Alpine.data("commentsSection", (targetUrl) => ({
|
||||||
@@ -40,9 +39,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
await this.checkOwner();
|
await this.checkOwner();
|
||||||
await this.loadComments();
|
await this.loadComments();
|
||||||
if (this.isOwner) {
|
if (this.isOwner) {
|
||||||
await this.loadOwnerReplies();
|
// Notify webmentions.js that owner is detected (for reply buttons)
|
||||||
// Notify webmentions.js that owner state + replies are ready
|
|
||||||
// (alpine:initialized fires before these async fetches resolve)
|
|
||||||
document.dispatchEvent(new CustomEvent("owner:detected"));
|
document.dispatchEvent(new CustomEvent("owner:detected"));
|
||||||
}
|
}
|
||||||
this.handleAuthReturn();
|
this.handleAuthReturn();
|
||||||
@@ -84,7 +81,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
Alpine.store("owner").syndicationTargets = this.syndicationTargets;
|
Alpine.store("owner").syndicationTargets = this.syndicationTargets;
|
||||||
|
|
||||||
// Note: owner:detected event is dispatched from init() after
|
// Note: owner:detected event is dispatched from init() after
|
||||||
// loadOwnerReplies() completes, so replies are available in the store
|
// this completes, so the Alpine store is populated before the event fires
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -92,20 +89,6 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadOwnerReplies() {
|
|
||||||
try {
|
|
||||||
const url = `/comments/api/owner-replies?target=${encodeURIComponent(this.targetUrl)}`;
|
|
||||||
const res = await fetch(url);
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
// Store for webmentions component to use via Alpine store
|
|
||||||
Alpine.store("owner").replies = data.children || [];
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Silently fail
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
startReply(commentId, platform, replyUrl, syndicateTo) {
|
startReply(commentId, platform, replyUrl, syndicateTo) {
|
||||||
this.replyingTo = { commentId, platform, replyUrl, syndicateTo };
|
this.replyingTo = { commentId, platform, replyUrl, syndicateTo };
|
||||||
this.replyText = "";
|
this.replyText = "";
|
||||||
|
|||||||
+99
-116
@@ -13,11 +13,6 @@
|
|||||||
|
|
||||||
if (!target || !domain) return;
|
if (!target || !domain) return;
|
||||||
|
|
||||||
// Extract site origin for filtering self-mentions
|
|
||||||
// (owner replies sent via webmention-sender appear as webmentions on own posts)
|
|
||||||
var siteOrigin = '';
|
|
||||||
try { siteOrigin = new URL(target).origin; } catch(e) {}
|
|
||||||
|
|
||||||
// Use server-side proxy to keep webmention.io token secure
|
// Use server-side proxy to keep webmention.io token secure
|
||||||
// Fetch both with and without trailing slash since webmention.io
|
// Fetch both with and without trailing slash since webmention.io
|
||||||
// stores targets inconsistently (Bridgy sends different formats)
|
// stores targets inconsistently (Bridgy sends different formats)
|
||||||
@@ -60,13 +55,9 @@
|
|||||||
function processWebmentions(allChildren) {
|
function processWebmentions(allChildren) {
|
||||||
if (!allChildren || !allChildren.length) return;
|
if (!allChildren || !allChildren.length) return;
|
||||||
|
|
||||||
// Filter out self-mentions (may exist in older cached data)
|
// Separate owner replies (threaded under parent) from regular interactions
|
||||||
allChildren = allChildren.filter(function(wm) {
|
var ownerReplies = allChildren.filter(function(wm) { return wm.is_owner && wm.parent_url; });
|
||||||
if (!siteOrigin) return true;
|
var regularItems = allChildren.filter(function(wm) { return !wm.is_owner; });
|
||||||
var source = wm['wm-source'] || wm.url || '';
|
|
||||||
return !source.startsWith(siteOrigin);
|
|
||||||
});
|
|
||||||
if (!allChildren.length) return;
|
|
||||||
|
|
||||||
let mentionsToShow;
|
let mentionsToShow;
|
||||||
if (hasBuildTimeSection) {
|
if (hasBuildTimeSection) {
|
||||||
@@ -94,7 +85,7 @@
|
|||||||
if (li.dataset.wmUrl) renderedReplies.add(li.dataset.wmUrl);
|
if (li.dataset.wmUrl) renderedReplies.add(li.dataset.wmUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
mentionsToShow = allChildren.filter(function(wm) {
|
mentionsToShow = regularItems.filter(function(wm) {
|
||||||
var prop = wm['wm-property'] || 'mention-of';
|
var prop = wm['wm-property'] || 'mention-of';
|
||||||
if (prop === 'in-reply-to') {
|
if (prop === 'in-reply-to') {
|
||||||
// Skip replies whose source URL is already rendered
|
// Skip replies whose source URL is already rendered
|
||||||
@@ -106,44 +97,108 @@
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No build-time section - show ALL webmentions from API
|
// No build-time section - show ALL regular webmentions from API
|
||||||
mentionsToShow = allChildren;
|
mentionsToShow = regularItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mentionsToShow.length) return;
|
if (mentionsToShow.length) {
|
||||||
|
// Group by type
|
||||||
|
const likes = mentionsToShow.filter((m) => m['wm-property'] === 'like-of');
|
||||||
|
const reposts = mentionsToShow.filter((m) => m['wm-property'] === 'repost-of');
|
||||||
|
const replies = mentionsToShow.filter((m) => m['wm-property'] === 'in-reply-to');
|
||||||
|
const mentions = mentionsToShow.filter((m) => m['wm-property'] === 'mention-of');
|
||||||
|
|
||||||
// Group by type
|
if (likes.length) {
|
||||||
const likes = mentionsToShow.filter((m) => m['wm-property'] === 'like-of');
|
appendAvatars('.webmention-likes .facepile, .webmention-likes .avatar-row', likes, 'likes');
|
||||||
const reposts = mentionsToShow.filter((m) => m['wm-property'] === 'repost-of');
|
updateCount('.webmention-likes h3', likes.length, 'Like');
|
||||||
const replies = mentionsToShow.filter((m) => m['wm-property'] === 'in-reply-to');
|
}
|
||||||
const mentions = mentionsToShow.filter((m) => m['wm-property'] === 'mention-of');
|
|
||||||
|
|
||||||
// Append new likes
|
if (reposts.length) {
|
||||||
if (likes.length) {
|
appendAvatars('.webmention-reposts .facepile, .webmention-reposts .avatar-row', reposts, 'reposts');
|
||||||
appendAvatars('.webmention-likes .facepile, .webmention-likes .avatar-row', likes, 'likes');
|
updateCount('.webmention-reposts h3', reposts.length, 'Repost');
|
||||||
updateCount('.webmention-likes h3', likes.length, 'Like');
|
}
|
||||||
|
|
||||||
|
if (replies.length) {
|
||||||
|
appendReplies('.webmention-replies ul', replies);
|
||||||
|
updateCount('.webmention-replies h3', replies.length, 'Repl', 'ies', 'y');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentions.length) {
|
||||||
|
appendMentions('.webmention-mentions ul', mentions);
|
||||||
|
updateCount('.webmention-mentions h3', mentions.length, 'Mention');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total count in main header
|
||||||
|
updateTotalCount(mentionsToShow.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append new reposts
|
// Thread owner replies under their parent interaction cards
|
||||||
if (reposts.length) {
|
threadOwnerReplies(ownerReplies);
|
||||||
appendAvatars('.webmention-reposts .facepile, .webmention-reposts .avatar-row', reposts, 'reposts');
|
}
|
||||||
updateCount('.webmention-reposts h3', reposts.length, 'Repost');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append new replies
|
function threadOwnerReplies(ownerReplies) {
|
||||||
if (replies.length) {
|
if (!ownerReplies || !ownerReplies.length) return;
|
||||||
appendReplies('.webmention-replies ul', replies);
|
|
||||||
updateCount('.webmention-replies h3', replies.length, 'Repl', 'ies', 'y');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append new mentions
|
ownerReplies.forEach(function(reply) {
|
||||||
if (mentions.length) {
|
var parentUrl = reply.parent_url;
|
||||||
appendMentions('.webmention-mentions ul', mentions);
|
if (!parentUrl) return;
|
||||||
updateCount('.webmention-mentions h3', mentions.length, 'Mention');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update total count in main header
|
// Find the interaction card whose URL matches the parent
|
||||||
updateTotalCount(mentionsToShow.length);
|
var matchingLi = document.querySelector('.webmention-replies li[data-wm-url="' + CSS.escape(parentUrl) + '"]');
|
||||||
|
if (!matchingLi) return;
|
||||||
|
|
||||||
|
var slot = matchingLi.querySelector('.wm-owner-reply-slot');
|
||||||
|
if (!slot) return;
|
||||||
|
|
||||||
|
// Skip if already rendered (dedup by reply URL)
|
||||||
|
if (slot.querySelector('[data-reply-url="' + CSS.escape(reply.url) + '"]')) return;
|
||||||
|
|
||||||
|
var replyCard = document.createElement('div');
|
||||||
|
replyCard.className = 'p-3 bg-surface-100 dark:bg-surface-900 rounded-lg border-l-2 border-amber-400 dark:border-amber-600';
|
||||||
|
replyCard.dataset.replyUrl = reply.url || '';
|
||||||
|
|
||||||
|
var innerDiv = document.createElement('div');
|
||||||
|
innerDiv.className = 'flex items-start gap-2';
|
||||||
|
|
||||||
|
var avatar = document.createElement('div');
|
||||||
|
avatar.className = 'w-6 h-6 rounded-full bg-amber-100 dark:bg-amber-900 flex-shrink-0 flex items-center justify-center text-xs font-bold';
|
||||||
|
avatar.textContent = (reply.author && reply.author.name ? reply.author.name[0] : 'O').toUpperCase();
|
||||||
|
|
||||||
|
var contentArea = document.createElement('div');
|
||||||
|
contentArea.className = 'flex-1';
|
||||||
|
|
||||||
|
var headerRow = document.createElement('div');
|
||||||
|
headerRow.className = 'flex items-center gap-2 flex-wrap';
|
||||||
|
|
||||||
|
var nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'font-medium text-sm';
|
||||||
|
nameSpan.textContent = (reply.author && reply.author.name) || 'Owner';
|
||||||
|
|
||||||
|
var authorBadge = document.createElement('span');
|
||||||
|
authorBadge.className = 'inline-flex items-center px-1.5 py-0.5 text-xs font-medium bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-full';
|
||||||
|
authorBadge.textContent = 'Author';
|
||||||
|
|
||||||
|
var timeEl = document.createElement('time');
|
||||||
|
timeEl.className = 'text-xs text-surface-600 dark:text-surface-400 font-mono';
|
||||||
|
timeEl.dateTime = reply.published || '';
|
||||||
|
timeEl.textContent = formatDate(reply.published);
|
||||||
|
|
||||||
|
headerRow.appendChild(nameSpan);
|
||||||
|
headerRow.appendChild(authorBadge);
|
||||||
|
headerRow.appendChild(timeEl);
|
||||||
|
|
||||||
|
var textDiv = document.createElement('div');
|
||||||
|
textDiv.className = 'mt-1 text-sm prose dark:prose-invert';
|
||||||
|
textDiv.textContent = (reply.content && reply.content.text) || '';
|
||||||
|
|
||||||
|
contentArea.appendChild(headerRow);
|
||||||
|
contentArea.appendChild(textDiv);
|
||||||
|
|
||||||
|
innerDiv.appendChild(avatar);
|
||||||
|
innerDiv.appendChild(contentArea);
|
||||||
|
replyCard.appendChild(innerDiv);
|
||||||
|
slot.appendChild(replyCard);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try cached data first (renders instantly on refresh)
|
// Try cached data first (renders instantly on refresh)
|
||||||
@@ -168,13 +223,6 @@
|
|||||||
const wmItems = [...(wmData1.children || []), ...(wmData2.children || [])];
|
const wmItems = [...(wmData1.children || []), ...(wmData2.children || [])];
|
||||||
const convItems = [...(convData1.children || []), ...(convData2.children || [])];
|
const convItems = [...(convData1.children || []), ...(convData2.children || [])];
|
||||||
|
|
||||||
// Filter out self-mentions (owner's own replies appearing as webmentions)
|
|
||||||
function isSelfMention(wm) {
|
|
||||||
if (!siteOrigin) return false;
|
|
||||||
var source = wm['wm-source'] || wm.url || '';
|
|
||||||
return source.startsWith(siteOrigin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build dedup sets from conversations items (richer metadata, take priority)
|
// Build dedup sets from conversations items (richer metadata, take priority)
|
||||||
const convUrls = new Set(convItems.map(c => c.url).filter(Boolean));
|
const convUrls = new Set(convItems.map(c => c.url).filter(Boolean));
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -182,7 +230,6 @@
|
|||||||
|
|
||||||
// Add conversations items first (they have platform provenance)
|
// Add conversations items first (they have platform provenance)
|
||||||
for (const wm of convItems) {
|
for (const wm of convItems) {
|
||||||
if (isSelfMention(wm)) continue;
|
|
||||||
const key = wm['wm-id'] || wm.url;
|
const key = wm['wm-id'] || wm.url;
|
||||||
if (key && !seen.has(key)) {
|
if (key && !seen.has(key)) {
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
@@ -198,9 +245,8 @@
|
|||||||
if (authorUrl) authorActions.add(authorUrl + '::' + action);
|
if (authorUrl) authorActions.add(authorUrl + '::' + action);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add webmention-io items, skipping duplicates and self-mentions
|
// Add webmention-io items, skipping duplicates
|
||||||
for (const wm of wmItems) {
|
for (const wm of wmItems) {
|
||||||
if (isSelfMention(wm)) continue;
|
|
||||||
const key = wm['wm-id'];
|
const key = wm['wm-id'];
|
||||||
if (seen.has(key)) continue;
|
if (seen.has(key)) continue;
|
||||||
// Also skip if same source URL exists in conversations
|
// Also skip if same source URL exists in conversations
|
||||||
@@ -765,72 +811,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show reply buttons and wire click handlers if owner is detected
|
// Show reply buttons when owner is detected
|
||||||
// Listen for custom event dispatched by comments.js after async owner check
|
// Listen for custom event dispatched by comments.js after async owner check
|
||||||
document.addEventListener('owner:detected', function() {
|
document.addEventListener('owner:detected', function() {
|
||||||
var ownerStore = Alpine.store && Alpine.store('owner');
|
|
||||||
if (!ownerStore || !ownerStore.isOwner) return;
|
|
||||||
|
|
||||||
wireReplyButtons();
|
wireReplyButtons();
|
||||||
|
|
||||||
// Render threaded owner replies under matching webmention cards
|
|
||||||
var ownerReplies = ownerStore.replies || [];
|
|
||||||
ownerReplies.forEach(function(reply) {
|
|
||||||
var inReplyTo = reply['in-reply-to'];
|
|
||||||
if (!inReplyTo) return;
|
|
||||||
|
|
||||||
// Find the webmention card whose URL matches
|
|
||||||
var matchingLi = document.querySelector('.webmention-replies li[data-wm-url="' + CSS.escape(inReplyTo) + '"]');
|
|
||||||
if (!matchingLi) return;
|
|
||||||
|
|
||||||
var slot = matchingLi.querySelector('.wm-owner-reply-slot');
|
|
||||||
if (!slot) return;
|
|
||||||
|
|
||||||
// Build owner reply card
|
|
||||||
var replyCard = document.createElement('div');
|
|
||||||
replyCard.className = 'p-3 bg-surface-100 dark:bg-surface-900 rounded-lg border-l-2 border-amber-400 dark:border-amber-600';
|
|
||||||
|
|
||||||
var innerDiv = document.createElement('div');
|
|
||||||
innerDiv.className = 'flex items-start gap-2';
|
|
||||||
|
|
||||||
var avatar = document.createElement('div');
|
|
||||||
avatar.className = 'w-6 h-6 rounded-full bg-amber-100 dark:bg-amber-900 flex-shrink-0 flex items-center justify-center text-xs font-bold';
|
|
||||||
avatar.textContent = (reply.author && reply.author.name ? reply.author.name[0] : 'O').toUpperCase();
|
|
||||||
|
|
||||||
var contentArea = document.createElement('div');
|
|
||||||
contentArea.className = 'flex-1';
|
|
||||||
|
|
||||||
var headerRow = document.createElement('div');
|
|
||||||
headerRow.className = 'flex items-center gap-2 flex-wrap';
|
|
||||||
|
|
||||||
var nameSpan = document.createElement('span');
|
|
||||||
nameSpan.className = 'font-medium text-sm';
|
|
||||||
nameSpan.textContent = (reply.author && reply.author.name) || 'Owner';
|
|
||||||
|
|
||||||
var authorBadge = document.createElement('span');
|
|
||||||
authorBadge.className = 'inline-flex items-center px-1.5 py-0.5 text-xs font-medium bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-full';
|
|
||||||
authorBadge.textContent = 'Author';
|
|
||||||
|
|
||||||
var timeEl = document.createElement('time');
|
|
||||||
timeEl.className = 'text-xs text-surface-600 dark:text-surface-400 font-mono';
|
|
||||||
timeEl.dateTime = reply.published || '';
|
|
||||||
timeEl.textContent = formatDate(reply.published);
|
|
||||||
|
|
||||||
headerRow.appendChild(nameSpan);
|
|
||||||
headerRow.appendChild(authorBadge);
|
|
||||||
headerRow.appendChild(timeEl);
|
|
||||||
|
|
||||||
var textDiv = document.createElement('div');
|
|
||||||
textDiv.className = 'mt-1 text-sm prose dark:prose-invert';
|
|
||||||
textDiv.textContent = (reply.content && reply.content.text) || '';
|
|
||||||
|
|
||||||
contentArea.appendChild(headerRow);
|
|
||||||
contentArea.appendChild(textDiv);
|
|
||||||
|
|
||||||
innerDiv.appendChild(avatar);
|
|
||||||
innerDiv.appendChild(contentArea);
|
|
||||||
replyCard.appendChild(innerDiv);
|
|
||||||
slot.appendChild(replyCard);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user