Compare commits

...
1 Commits
Author SHA1 Message Date
Laan Tungir 6d39188967 Fix post-feed.html: remove collapse, fix missing reply, move composer to top
1. Removed collapse/expand functionality — all replies shown, no hidden counts
2. Fixed missing last reply — added safety check to ensure all fetched replies are rendered
3. Moved reply composer to top (under main post, before replies list)
2026-06-25 20:00:43 -04:00
2 changed files with 64 additions and 175 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.26",
"VERSION_NUMBER": "0.7.26",
"BUILD_DATE": "2026-06-25T23:53:36.767Z"
"VERSION": "v0.7.27",
"VERSION_NUMBER": "0.7.27",
"BUILD_DATE": "2026-06-26T00:00:43.641Z"
}
+61 -172
View File
@@ -148,37 +148,6 @@
background: var(--accent-color);
}
/* Click target overlaying each line so users can collapse a subtree
by clicking the vertical line for that level. */
.reply-thread-line-hit {
position: absolute;
top: 0;
bottom: 0;
width: 12px;
cursor: pointer;
z-index: 1;
}
.reply-collapse-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
margin: 2px 0 4px 0;
font-size: 80%;
color: var(--muted-color);
background: var(--secondary-color);
border: 1px solid var(--muted-color);
border-radius: 8px;
cursor: pointer;
user-select: none;
}
.reply-collapse-toggle:hover {
color: var(--accent-color);
border-color: var(--accent-color);
}
.reply-item-focused {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
@@ -205,10 +174,10 @@
<div id="divHint">Loading thread…</div>
<div id="divThread">
<div id="divOriginalPost"></div>
<div id="divComposer"></div>
<div id="divThreadHeader">Replies</div>
<div id="divReplies"></div>
</div>
<div id="divComposer"></div>
</div>
<div id="divFooter">
@@ -349,7 +318,6 @@
const replies = []; // flat list of reply events
const replyIds = new Set(); // dedupe by event id
const renderedReplyIds = new Set();
const collapsedReplyIds = new Set(); // Phase 2.5 — collapsed subtree roots (in-memory)
const focusedEventId = (urlParams.get('focus') || '').trim().toLowerCase() || null;
let hamburgerInstance = null;
@@ -742,80 +710,71 @@
to the next sibling. This makes vertical lines visually
contiguous — a reply's descendants appear directly below it.
Returns an array of { event, level } in display order, skipping
descendants of any event id in `collapsedIds`.
Returns an array of { event, level } in display order.
---------------------------------------------------------------- */
function buildDepthFirstOrder(events, levels, rootEventId, collapsedIds) {
const childrenOf = new Map(); // parentId -> [event, ...]
const rootChildren = [];
function buildDepthFirstOrder(events, levels, rootEventId) {
const childrenOf = new Map(); // parentId -> [event, ...]
for (const evt of events) {
if (!evt?.id || evt.id === rootEventId) continue;
const pid = getDirectParentId(evt);
let parentKey;
if (pid && events.some((e) => e?.id === pid)) {
parentKey = pid;
} else {
// Parent not in set — group under root.
parentKey = rootEventId;
}
if (!childrenOf.has(parentKey)) childrenOf.set(parentKey, []);
childrenOf.get(parentKey).push(evt);
}
for (const evt of events) {
if (!evt?.id || evt.id === rootEventId) continue;
const pid = getDirectParentId(evt);
let parentKey;
if (pid && events.some((e) => e?.id === pid)) {
parentKey = pid;
} else {
// Parent not in set — group under root.
parentKey = rootEventId;
}
if (!childrenOf.has(parentKey)) childrenOf.set(parentKey, []);
childrenOf.get(parentKey).push(evt);
}
// Sort each child group chronologically (oldest first).
for (const arr of childrenOf.values()) {
arr.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
}
// Sort each child group chronologically (oldest first).
for (const arr of childrenOf.values()) {
arr.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
}
const ordered = [];
const visited = new Set();
const ordered = [];
const visited = new Set();
function visit(parentId, level) {
const kids = childrenOf.get(parentId) || [];
for (const child of kids) {
if (visited.has(child.id)) continue;
visited.add(child.id);
const childLevel = levels.get(child.id) ?? level + 1;
ordered.push({ event: child, level: childLevel });
if (!collapsedIds.has(child.id)) {
visit(child.id, childLevel);
}
}
}
function visit(parentId, level) {
const kids = childrenOf.get(parentId) || [];
for (const child of kids) {
if (visited.has(child.id)) continue;
visited.add(child.id);
const childLevel = levels.get(child.id) ?? level + 1;
ordered.push({ event: child, level: childLevel });
visit(child.id, childLevel);
}
}
visit(rootEventId, 0);
return ordered;
}
visit(rootEventId, 0);
/* ----------------------------------------------------------------
Count descendants of a given event id (for the "N replies hidden"
label). Uses the same children map logic.
---------------------------------------------------------------- */
function countDescendants(eventId, events) {
const childrenOf = new Map();
for (const evt of events) {
if (!evt?.id || evt.id === targetEventId) continue;
const pid = getDirectParentId(evt);
const parentKey = pid && events.some((e) => e?.id === pid) ? pid : targetEventId;
if (!childrenOf.has(parentKey)) childrenOf.set(parentKey, []);
childrenOf.get(parentKey).push(evt);
}
let count = 0;
const stack = [eventId];
const seen = new Set();
while (stack.length) {
const cur = stack.pop();
if (seen.has(cur)) continue;
seen.add(cur);
const kids = childrenOf.get(cur) || [];
for (const k of kids) {
count += 1;
stack.push(k.id);
}
}
return count;
}
// Debug check: make sure every event in the input set appears in
// the ordered list. If any are missing (e.g. an orphaned reply
// whose parent is not in the fetched set but was somehow not
// grouped under root), log a warning and append them at the end
// so no reply is silently dropped.
const orderedIds = new Set(ordered.map((o) => o.event?.id));
const missing = [];
for (const evt of events) {
if (!evt?.id || evt.id === rootEventId) continue;
if (!orderedIds.has(evt.id)) {
missing.push(evt);
}
}
if (missing.length > 0) {
console.warn(
`[post-feed.html] ${missing.length} reply event(s) missing from depth-first order — appending at end:`,
missing.map((e) => e.id)
);
for (const evt of missing) {
ordered.push({ event: evt, level: levels.get(evt.id) ?? 1 });
}
}
return ordered;
}
/* ----------------------------------------------------------------
renderReplies() — threaded rendering with vertical indent lines
@@ -831,11 +790,9 @@
}
const levels = computeReplyLevels(replies, targetEventId);
const ordered = buildDepthFirstOrder(replies, levels, targetEventId, collapsedReplyIds);
const ordered = buildDepthFirstOrder(replies, levels, targetEventId);
const visibleCount = ordered.length;
const hiddenCount = replies.length - visibleCount;
const totalLabel = `${replies.length} ${replies.length === 1 ? 'reply' : 'replies'}` + (hiddenCount > 0 ? ` (${hiddenCount} hidden)` : '');
const totalLabel = `${replies.length} ${replies.length === 1 ? 'reply' : 'replies'}`;
divThreadHeader.textContent = totalLabel;
const wiredIds = [];
@@ -864,26 +821,6 @@
line.classList.add('selected');
}
container.appendChild(line);
// Click target on the line area — clicking the line for level i
// collapses the subtree of the ancestor at that level. We find
// the ancestor by walking up the parent chain i times.
const hit = document.createElement('div');
hit.className = 'reply-thread-line-hit';
hit.style.left = `${i * 12}px`;
hit.title = 'Click to collapse/expand this branch';
hit.addEventListener('click', (ev) => {
ev.stopPropagation();
const ancestorId = findAncestorAtLevel(reply.id, i, levels);
if (!ancestorId) return;
if (collapsedReplyIds.has(ancestorId)) {
collapsedReplyIds.delete(ancestorId);
} else {
collapsedReplyIds.add(ancestorId);
}
renderReplies();
});
container.appendChild(hit);
}
// The post card itself.
@@ -894,28 +831,6 @@
}
container.appendChild(replyEl);
// Collapse toggle button for replies that have children.
const descendantCount = countDescendants(reply.id, replies);
if (descendantCount > 0) {
const toggle = document.createElement('button');
toggle.className = 'reply-collapse-toggle';
toggle.type = 'button';
const collapsed = collapsedReplyIds.has(reply.id);
toggle.textContent = collapsed
? `${descendantCount} ${descendantCount === 1 ? 'reply' : 'replies'} hidden`
: `▾ collapse`;
toggle.addEventListener('click', (ev) => {
ev.stopPropagation();
if (collapsedReplyIds.has(reply.id)) {
collapsedReplyIds.delete(reply.id);
} else {
collapsedReplyIds.add(reply.id);
}
renderReplies();
});
container.appendChild(toggle);
}
divReplies.appendChild(container);
renderedReplyIds.add(reply.id);
wiredIds.push(reply.id);
@@ -935,32 +850,6 @@
}
}
/* ----------------------------------------------------------------
findAncestorAtLevel(eventId, targetLevel, levels)
----------------------------------------------------------------
Walks up the parent chain to find the ancestor of `eventId` that
sits at `targetLevel`. Used so clicking a vertical line at
position i collapses the ancestor whose subtree that line
represents.
---------------------------------------------------------------- */
function findAncestorAtLevel(eventId, targetLevel, levels) {
let cur = eventId;
const byId = new Map(replies.map((r) => [r.id, r]));
if (originalPost) byId.set(originalPost.id, originalPost);
const guard = new Set();
while (cur && !guard.has(cur)) {
guard.add(cur);
const curLevel = levels.get(cur);
if (curLevel === targetLevel) return cur;
const evt = byId.get(cur);
if (!evt) return null;
const pid = getDirectParentId(evt);
if (!pid) return null;
cur = pid;
}
return null;
}
function appendReplyIfNew(evt) {
if (!upsertReply(evt)) return;
renderReplies();