replies working, need to add root post ID

This commit is contained in:
bobbydigitales
2026-04-16 00:26:20 -07:00
parent 9c15ed2cd2
commit f22d8b9ba6
12 changed files with 374 additions and 56 deletions

View File

@@ -1,8 +1,8 @@
import { generateID } from "IDUtils";
import { PeerManager, PeerEventTypes } from "PeerManager";
import { Sync } from "Sync";
import { openDatabase, getData, addData, deleteData, getAllData, getPostForUser } from "db";
import { arrayBufferToBase64, compressString } from "dataUtils";
import { openDatabase, getData, addData, deleteData, mergeDataArray, getAllData, getPostForUser, getRepliesForPost, buildReplyCountMap } from "db";
import { arrayBufferToBase64, compressString, decompressBuffer, base64ToArrayBuffer } from "dataUtils";
import { log, logID, renderLog, setLogVisibility } from "log";
class Post {
constructor(author, author_id, text, post_timestamp, imageData = null, importedFrom = null, importSource = null, reply_to_id = null) {
@@ -382,7 +382,20 @@ export class App {
document.body.removeChild(link);
globalThis.URL.revokeObjectURL(url);
}
async importPostsForUser(userID, posts) {
async importPostsForUser(userID, buffer) {
console.log.apply(null, log("Importing posts"));
const json = await decompressBuffer(buffer);
const posts = JSON.parse(json);
for (let post of posts) {
if (post.image_data && typeof post.image_data === 'string') {
post.image_data = await base64ToArrayBuffer(post.image_data);
}
if (post.post_timestamp && typeof post.post_timestamp === 'string') {
post.post_timestamp = new Date(post.post_timestamp);
}
}
await mergeDataArray(userID, posts);
console.log.apply(null, log(`Imported ${posts.length} posts`));
}
async exportPostsForUser(userID) {
let posts = await getAllData(userID);
@@ -725,6 +738,16 @@ export class App {
let navContainer = this.div('nav-container');
let burgerMenuButton = this.div('burger-menu-button');
burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
let importFilePicker = document.getElementById('import-file-input');
importFilePicker?.addEventListener('change', async () => {
const file = importFilePicker.files?.[0];
if (!file)
return;
const buffer = await file.arrayBuffer();
await this.importPostsForUser(this.userID, buffer);
importFilePicker.value = '';
this.render();
});
let exportButton = this.button("export-button");
exportButton.addEventListener('click', async (e) => {
await this.exportPostsForUser(this.userID);
@@ -737,7 +760,7 @@ export class App {
filePicker?.addEventListener('change', async (event) => {
for (let file of filePicker.files) {
let buffer = await file.arrayBuffer();
await this.createPost(this.userID, 'image...', buffer, file.type);
await this.createPost(this.userID, 'image...', buffer, file.type, this.replyToID);
}
// Reset so that if they pick the same image again, we still get the change event.
filePicker.value = '';
@@ -781,11 +804,18 @@ export class App {
const dataTransfer = e.clipboardData;
const file = dataTransfer.files[0];
let buffer = await file.arrayBuffer();
await this.createPost(this.userID, 'image...', buffer, file.type);
await this.createPost(this.userID, 'image...', buffer, file.type, this.replyToID);
});
postButton.addEventListener("click", () => {
const submitPost = () => {
this.createPost(userID, postText.value, undefined, undefined, this.replyToID);
this.exitCompose();
};
postButton.addEventListener("click", submitPost);
postText.addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
e.preventDefault();
submitPost();
}
});
// updateApp.addEventListener("click", () => {
// registration?.active?.postMessage({ type: "update_app" });
@@ -796,17 +826,18 @@ export class App {
}
// Change this all to a template so we're not toggling state in this crazy way!
enterCompose(replyToID = null) {
this.replyToID = replyToID;
if (replyToID) {
this.renderComposeReplyArea(replyToID);
document.getElementById("compose-reply-area").style.display = "block";
}
replyToID = replyToID;
document.getElementById('compose').style.display = 'block';
document.getElementById('textarea_post')?.focus();
document.getElementById('compose-dimmer')?.classList.add("compose-dimmer-dimmed");
document.body.classList.add("no-scroll");
}
exitCompose() {
this.replyToID = null;
let postText = document.getElementById("textarea_post");
postText.value = "";
document.getElementById('compose').style.display = 'none';
@@ -942,6 +973,7 @@ export class App {
this.sync.setArchive(this.isArchivePeer);
this.connect();
await this.initDB();
window.addEventListener('popstate', () => { this.getRoute(); this.render(); });
this.connectURL = `${document.location.origin}/connect/${this.userID}`;
document.getElementById('connectURL').innerHTML = `<a href="${this.connectURL}">connect</a>`;
let time = 0;
@@ -1074,13 +1106,18 @@ export class App {
const fragment = document.createDocumentFragment();
contentDiv.innerHTML = "";
let count = 0;
const isPostView = this.router.route === App.Route.POST;
contentDiv.classList.toggle('post-view', isPostView);
const replyCountMap = await buildReplyCountMap();
this.renderedPosts.clear();
let first = true;
for (let i = this.posts.length - 1; i >= 0; i--) {
let postData = this.posts[i];
if (!isPostView && postData.data.reply_to_id)
continue;
// this.postsSet.add(postData);
// TODO return promises for all image loads and await those.
let post = this.renderPost(postData.data, first);
let post = this.renderPost(postData.data, first, replyCountMap.recursive.get(postData.data.post_id) ?? 0);
first = false;
// this.renderedPosts.set(postData.post_id, post);
if (post) {
@@ -1095,6 +1132,28 @@ export class App {
throw new Error("Couldn't get content div!");
}
contentDiv.appendChild(fragment);
if (isPostView && this.posts.length > 0) {
const renderReplies = async (postID, depth) => {
if (depth > 2)
return;
const replies = await getRepliesForPost(postID);
// Top-level replies: newest first. Nested replies: oldest first (readable order).
if (depth === 0) {
replies.sort((a, b) => new Date(b.data.post_timestamp).getTime() - new Date(a.data.post_timestamp).getTime());
}
else {
replies.sort((a, b) => new Date(a.data.post_timestamp).getTime() - new Date(b.data.post_timestamp).getTime());
}
for (const reply of replies) {
const el = this.renderPost(reply.data, false, replyCountMap.direct.get(reply.data.post_id) ?? 0);
if (depth > 0)
el.style.marginLeft = `${depth * 20}px`;
contentDiv.appendChild(el);
await renderReplies(reply.data.post_id, depth + 1);
}
};
await renderReplies(this.posts[0].data.post_id, 0);
}
let renderTime = this.timerDelta();
console.log.apply(null, log(`render took: ${renderTime.toFixed(2)}ms`));
;
@@ -1113,7 +1172,7 @@ export class App {
composeReplyArea.innerText = replyToID;
composeReplyArea.classList.add("show");
}
renderPost(post, first) {
renderPost(post, first, replyCount = 0) {
if (!(post.hasOwnProperty("text"))) {
throw new Error("Post is malformed!");
}
@@ -1130,7 +1189,7 @@ export class App {
await navigator.clipboard.writeText(shareUrl);
};
let replyButton = document.createElement('button');
replyButton.innerText = 'reply';
replyButton.innerText = replyCount > 0 ? `reply (${replyCount})` : 'reply';
replyButton.onclick = async () => {
console.log(`replying to post ${post.post_id}`);
this.enterCompose(post.post_id);
@@ -1150,24 +1209,32 @@ export class App {
markdown = markdown.replace("<iframe", `<iframe style="width:100%;height:50px;display:none" onblur="this.style.display = 'inline';"`);
}
let userURL = `${document.location.origin}/user/${post.author_id}/`;
let postTemplate = `<div>${first ? '' : '<hr>'}
<div>
<span class='header' title='${timestamp}'><img class="logo" src="/static/favicon.ico"><a class="username" href="${userURL}">@${post.author}</a> -
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
</span>
let postTemplate = `<div class="post-container">${first ? '' : '<hr>'}
<div class="post-body">
<div>
<span class='header' title='${timestamp}'><img class="logo" src="/static/favicon.ico"><a class="username" href="${userURL}">@${post.author}</a> -
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
</span>
</div>
<div>${markdown}</div>
<div id="image"></div>
<span id="replyButton"></span>
${ownPost ? `<span id="editButton"></span>` : ''}
<span id="shareButton"></span>
${ownPost ? `<span id="deleteButton"></span>` : ''}
</div>
<div>${markdown}</div>
<div id="image"></div>
<span id="replyButton"></span>
${ownPost ? `<span id="editButton"></span>` : ''}
<span id="shareButton"></span>
${ownPost ? `<span id="deleteButton"></span>` : ''}
</div>`;
containerDiv.innerHTML = postTemplate;
const postBody = containerDiv.querySelector('.post-body');
postBody.addEventListener('click', (e) => {
if (e.target.closest('button, a'))
return;
history.pushState({}, '', `/user/${post.author_id}/post/${post.post_id}`);
this.getRoute();
this.render();
});
if (ownPost) {
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
@@ -1221,6 +1288,12 @@ export class App {
}
}
}
else {
this.router.route = App.Route.HOME;
this.router.userID = '';
this.router.postID = '';
this.router.mediaID = '';
}
console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route]));
// user = /user/<ID>
// post = /user/<ID>/post/<ID>