This commit is contained in:
2026-04-16 02:02:34 -07:00
21 changed files with 336 additions and 82 deletions

View File

@@ -1,7 +1,7 @@
import { generateID } from "IDUtils";
import { PeerManager, PeerEventTypes } from "PeerManager";
import { Sync } from "Sync";
import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds, getPostForUser, getRepliesForPost, buildReplyCountMap } from "db";
import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds, getPostForUser, getPostById, getRepliesForPost, buildReplyCountMap } from "db";
import { arrayBufferToBase64, compressString, decompressBuffer, base64ToArrayBuffer } from "dataUtils";
import { log, logID, renderLog, setLogVisibility } from "log"
@@ -14,6 +14,7 @@ class Post {
post_timestamp: Date;
post_id: string;
reply_to_id: string|null;
root_id: string|null;
author: string;
author_id: string;
text: string;
@@ -29,7 +30,8 @@ class Post {
imageData: ArrayBuffer | null = null,
importedFrom: "twitter" | null = null,
importSource: any = null,
reply_to_id:string|null = null) {
reply_to_id: string|null = null,
root_id: string|null = null) {
this.post_timestamp = post_timestamp;
this.post_id = generateID();
@@ -42,6 +44,7 @@ class Post {
this.importedFrom = importedFrom;
this.importSource = importSource;
this.reply_to_id = reply_to_id;
this.root_id = root_id;
}
}
@@ -120,6 +123,7 @@ export class App {
userID: string = '';
peerID: string = '';
replyToID: string|null = null;
replyRootID: string|null = null;
following: Set<string> = new Set();
posts: StoragePost[] = [];
isHeadless: boolean = false;
@@ -737,7 +741,7 @@ export class App {
}
}
async createPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/svg+xml" | "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4", replyToID:string|null = null) {
async createPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/svg+xml" | "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4", replyToID: string|null = null, replyRootID: string|null = null) {
if ((typeof postText !== "string") || postText.length === 0) {
console.log.apply(null, log("Not posting an empty string..."));
return;
@@ -752,7 +756,7 @@ export class App {
}
}
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID);
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID, replyRootID);
// this.posts.push(post);
// localStorage.setItem(key, JSON.stringify(posts));
addData(userID, post);
@@ -763,27 +767,23 @@ export class App {
this.render();
}
getPeerID() {
let id = localStorage.getItem("peer_id");
if (!id) {
console.log.apply(null, log(`Didn't find a peer ID, generating one`));;
id = generateID();
localStorage.setItem("peer_id", id);
}
getPeerID(): string {
const existing = localStorage.getItem("peer_id");
if (existing) return existing;
console.log.apply(null, log(`Didn't find a peer ID, generating one`));
const id = generateID();
localStorage.setItem("peer_id", id);
return id;
}
getUserID() {
let id = localStorage.getItem("dandelion_id");
if (!id) {
console.log.apply(null, log(`Didn't find a user ID, generating one`));;
id = generateID();
localStorage.setItem("dandelion_id", id);
}
getUserID(): string {
const existing = localStorage.getItem("dandelion_id");
if (existing) return existing;
console.log.apply(null, log(`Didn't find a user ID, generating one`));
const id = generateID();
localStorage.setItem("dandelion_id", id);
return id;
}
@@ -1016,7 +1016,7 @@ export class App {
filePicker?.addEventListener('change', async (event: any) => {
for (let file of filePicker.files as any) {
let buffer = await file.arrayBuffer();
await this.createPost(this.userID, 'image...', buffer, file.type, this.replyToID);
await this.createPost(this.userID, 'image...', buffer, file.type, this.replyToID, this.replyRootID);
}
// Reset so that if they pick the same image again, we still get the change event.
@@ -1075,11 +1075,11 @@ 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 as any, this.replyToID);
await this.createPost(this.userID, 'image...', buffer, file.type as any, this.replyToID, this.replyRootID);
});
const submitPost = () => {
this.createPost(userID, postText.value, undefined, undefined, this.replyToID);
this.createPost(userID, postText.value, undefined, undefined, this.replyToID, this.replyRootID);
this.exitCompose();
};
@@ -1105,8 +1105,9 @@ export class App {
// Change this all to a template so we're not toggling state in this crazy way!
enterCompose(replyToID:string|null=null) {
enterCompose(replyToID: string|null = null, replyRootID: string|null = null) {
this.replyToID = replyToID;
this.replyRootID = replyRootID;
if (replyToID) {
this.renderComposeReplyArea(replyToID);
@@ -1121,6 +1122,7 @@ export class App {
exitCompose() {
this.replyToID = null;
this.replyRootID = null;
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
postText.value = "";
document.getElementById('compose')!.style.display = 'none';
@@ -1532,6 +1534,15 @@ export class App {
contentDiv.appendChild(fragment);
if (isPostView && this.posts.length > 0) {
const currentPost = this.posts[0].data;
if (currentPost.root_id) {
const rootRecord = await getPostById(currentPost.root_id);
if (rootRecord) {
const rootEl = this.renderPost(rootRecord.data, true, replyCountMap.recursive.get(rootRecord.data.post_id) ?? 0);
contentDiv.insertBefore(rootEl, contentDiv.firstChild);
}
}
const renderReplies = async (postID: string, depth: number) => {
if (depth > 2) return;
const replies = await getRepliesForPost(postID);
@@ -1552,7 +1563,7 @@ export class App {
await renderReplies(reply.data.post_id, depth + 1);
}
};
await renderReplies(this.posts[0].data.post_id, 0);
await renderReplies(this.posts[0].data.post_id, currentPost.root_id ? 1 : 0);
}
let renderTime = this.timerDelta();
@@ -1575,10 +1586,31 @@ export class App {
this.render();
}
renderComposeReplyArea(replyToID:string) {
let composeReplyArea = document.getElementById('compose-reply-area') as HTMLElement;
composeReplyArea.innerText = replyToID;
composeReplyArea.classList.add("show");
async renderComposeReplyArea(replyToID: string) {
const composeReplyArea = document.getElementById('compose-reply-area') as HTMLElement;
composeReplyArea.innerHTML = '';
const record = await getPostById(replyToID);
if (!record) return;
const postData = record.data;
const previewDiv = document.createElement('div');
previewDiv.className = 'compose-reply-preview';
const textDiv = document.createElement('div');
textDiv.className = 'compose-reply-preview-text';
textDiv.innerHTML = this.markedAvailable ? marked.parse(postData.text) : postData.text;
previewDiv.appendChild(textDiv);
if (postData.image_data) {
const img = document.createElement('img');
img.className = 'compose-reply-preview-image';
const blob = new Blob([postData.image_data as ArrayBuffer]);
img.src = URL.createObjectURL(blob);
previewDiv.appendChild(img);
}
composeReplyArea.appendChild(previewDiv);
}
renderPost(post: Post, first: boolean, replyCount: number = 0) {
@@ -1603,10 +1635,8 @@ export class App {
let replyButton = document.createElement('button'); replyButton.innerText = replyCount > 0 ? `reply (${replyCount})` : 'reply';
replyButton.onclick = async () => {
console.log(`replying to post ${post.post_id}`);
this.enterCompose(post.post_id);
// let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
// await navigator.clipboard.writeText(shareUrl)
const rootID = post.root_id ?? post.post_id;
this.enterCompose(post.post_id, rootID);
};

View File

@@ -2,7 +2,7 @@ import { openDatabase, getData, addData, addDataArray, clearData, deleteData, me
import { log, logID } from "log";
async function bytesToBase64DataUrl(bytes: Uint8Array, type = "application/octet-stream") {
async function bytesToBase64DataUrl(bytes: Uint8Array<ArrayBuffer>, type = "application/octet-stream") {
return await new Promise((resolve, reject) => {
const reader = Object.assign(new FileReader(), {
onload: () => resolve(reader.result),

View File

@@ -1,4 +1,4 @@
export async function bytesToBase64DataUrl(bytes: Uint8Array, type = "application/octet-stream") {
export async function bytesToBase64DataUrl(bytes: Uint8Array<ArrayBuffer>, type = "application/octet-stream") {
return await new Promise((resolve, reject) => {
const reader = Object.assign(new FileReader(), {
onload: () => resolve(reader.result),

View File

@@ -505,6 +505,24 @@ export async function buildReplyCountMap(): Promise<{ direct: Map<string, number
return { direct, recursive };
}
export async function getPostById(postID: string): Promise<any | undefined> {
const knownUsers = [...(await indexedDB.databases())]
.map(db => db.name?.replace('user_', '')).filter(Boolean) as string[];
for (const userID of knownUsers) {
try {
const { store } = await getDBTransactionStore(userID);
const index = store.index("postIDIndex");
const result: any = await new Promise((resolve, reject) => {
const req = index.get(postID);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
if (result) return result;
} catch (_) {}
}
return undefined;
}
export async function getRepliesForPost(postID: string): Promise<any[]> {
const knownUsers = [...(await indexedDB.databases())]
.map(db => db.name?.replace('user_', '')).filter(Boolean) as string[];

View File

@@ -230,7 +230,7 @@ window.addEventListener('scroll', () => {
// }
// }
async function bytesToBase64DataUrl(bytes: Uint8Array, type = "application/octet-stream") {
async function bytesToBase64DataUrl(bytes: Uint8Array<ArrayBuffer>, type = "application/octet-stream") {
return await new Promise((resolve, reject) => {
const reader = Object.assign(new FileReader(), {
onload: () => resolve(reader.result),
@@ -918,7 +918,7 @@ class App {
}_${String(d.getSeconds()).padStart(2, '0')}`;
this.downloadBinary(compressedData, `ddln_${this.username}_export_${timestamp}.json.gz`);
this.downloadBinary(compressedData.buffer, `ddln_${this.username}_export_${timestamp}.json.gz`);
}
async importTweetArchive(userID: string, tweetArchive: any[]) {