working on replies

This commit is contained in:
2026-04-15 06:05:46 +00:00
parent 0d392c90cc
commit c1de283fb6
15 changed files with 436 additions and 211 deletions

View File

@@ -13,12 +13,11 @@ type PeerID = string;
class Post {
post_timestamp: Date;
post_id: string;
reply_to_id: string|null;
author: string;
author_id: string;
text: string;
image_data: ArrayBuffer | null;
importedFrom: "twitter" | null;
importSource: any;
@@ -29,7 +28,8 @@ class Post {
post_timestamp: Date,
imageData: ArrayBuffer | null = null,
importedFrom: "twitter" | null = null,
importSource: any = null) {
importSource: any = null,
reply_to_id:string|null = null) {
this.post_timestamp = post_timestamp;
this.post_id = generateID();
@@ -41,6 +41,7 @@ class Post {
this.importedFrom = importedFrom;
this.importSource = importSource;
this.reply_to_id = reply_to_id;
}
}
@@ -118,6 +119,7 @@ export class App {
peername: string = '';
userID: string = '';
peerID: string = '';
replyToID: string|null = null;
following: Set<string> = new Set();
posts: StoragePost[] = [];
isHeadless: boolean = false;
@@ -342,23 +344,16 @@ export class App {
}
return true;
// return posts;
// return postIDs;
});
this.peerManager.registerRPC('sendPostForUser', async (sendingPeerID: string, userID: string, post: Post) => {
console.log.apply(null, log(`[app] sendPostForUser got post[${logID(post.post_id)}] from peer[${logID(sendingPeerID)}] for user[${logID(userID)}] author[${post.author}] text[${post.text}]`));
// if (post.text === "image...") {
// debugger;
// }
let peerData = this.statusBar.getPeerData(sendingPeerID);
if (peerData) {
this.statusBar.updatePeerMessage(sendingPeerID, `⬇️${logID(userID)} ${peerData.havePostCount}/${peerData.neededPostCount}}`);
}
await this.sync.writePostForUser(userID, post);
// if (userID === this.userID) {
if (peerData) {
peerData.havePostCount++
@@ -372,14 +367,13 @@ export class App {
this.renderTimer = setTimeout(() => { this.render() }, 1000);
return true;
// }
});
this.statusBar.setMessageHTML("Connecting to ddln network...")
this.statusBar.setMessageHTML("Connecting to ddln...")
await this.peerManager.connect();
console.log.apply(null, log("*************** after peerManager.connect"));;
this.statusBar.setMessageHTML("Connected to ddln network...")
this.statusBar.setMessageHTML("Connected to ddln.")
@@ -729,7 +723,7 @@ export class App {
}
}
async createNewPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4") {
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) {
if ((typeof postText !== "string") || postText.length === 0) {
console.log.apply(null, log("Not posting an empty string..."));
return;
@@ -737,14 +731,14 @@ export class App {
if (mediaData &&
(mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
(mediaData as ArrayBuffer).byteLength > 500 * 1024) {
(mediaData as ArrayBuffer).byteLength > 256 * 1024) {
let compressedImage = await this.compressImage(mediaData as ArrayBuffer, mimeType, 0.9);
if (compressedImage) {
mediaData = compressedImage as ArrayBuffer;
}
}
let post = new Post(this.username, userID, postText, new Date(), mediaData);
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID);
// this.posts.push(post);
// localStorage.setItem(key, JSON.stringify(posts));
addData(userID, post);
@@ -990,8 +984,7 @@ export class App {
let composeButton = this.div('compose-button');
composeButton.addEventListener('click', e => {
document.getElementById('compose')!.style.display = 'block';
document.getElementById('textarea_post')?.focus();
this.enterCompose();
});
@@ -999,11 +992,13 @@ export class App {
filePicker?.addEventListener('change', async (event: any) => {
for (let file of filePicker.files as any) {
let buffer = await file.arrayBuffer();
await this.createNewPost(this.userID, 'image...', buffer, file.type);
await this.createPost(this.userID, 'image...', buffer, file.type);
}
// Reset so that if they pick the same image again, we still get the change event.
filePicker.value = '';
this.exitCompose();
});
let filePickerLabel = document.getElementById('file-input-label');
@@ -1040,25 +1035,28 @@ export class App {
// clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() });
let postButton = document.getElementById("button_post") as HTMLButtonElement;
let cancelPostButton = document.getElementById("button_cancel_post") as HTMLElement;
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
if (!(postButton && postText)) {
let postButton = document.getElementById("button_post") as HTMLButtonElement;
if (!(cancelPostButton && postButton && postText)) {
throw new Error();
}
cancelPostButton.addEventListener('click', (e) => {
this.exitCompose();
});
postText.addEventListener('paste', async (e) => {
const dataTransfer = e.clipboardData
const file = dataTransfer!.files[0];
let buffer = await file.arrayBuffer();
await this.createNewPost(this.userID, 'image...', buffer, file.type as any);
await this.createPost(this.userID, 'image...', buffer, file.type as any);
});
postButton.addEventListener("click", () => {
this.createNewPost(userID, postText.value);
postText.value = "";
document.getElementById('compose')!.style.display = 'none';
this.createPost(userID, postText.value, undefined, undefined, this.replyToID);
this.exitCompose();
});
// updateApp.addEventListener("click", () => {
@@ -1072,6 +1070,31 @@ export class App {
}
// Change this all to a template so we're not toggling state in this crazy way!
enterCompose(replyToID:string|null=null) {
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() {
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
postText.value = "";
document.getElementById('compose')!.style.display = 'none';
document.getElementById('compose-dimmer')?.classList.remove("compose-dimmer-dimmed");
document.getElementById("compose-reply-area")!.style.display = "none";
document.body.classList.remove("no-scroll");
}
async getPostsForFeed() {
// get N posts from each user and sort them by date.
@@ -1486,6 +1509,12 @@ export class App {
this.render();
}
renderComposeReplyArea(replyToID:string) {
let composeReplyArea = document.getElementById('compose-reply-area') as HTMLElement;
composeReplyArea.innerText = replyToID;
composeReplyArea.classList.add("show");
}
renderPost(post: Post, first: boolean) {
if (!(post.hasOwnProperty("text"))) {
throw new Error("Post is malformed!");
@@ -1505,6 +1534,16 @@ export class App {
await navigator.clipboard.writeText(shareUrl)
};
let replyButton = document.createElement('button'); replyButton.innerText = '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)
};
let ownPost = post.author_id === this.userID;
let markdown = post.text;
@@ -1530,11 +1569,17 @@ export class App {
<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>
${ownPost ? `<span id="deleteButton"></span>` : ''}
${ownPost ? `<span id="editButton"></span>` : ''}
<span id="shareButton"></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;
@@ -1545,33 +1590,33 @@ export class App {
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
}
containerDiv.querySelector('#shareButton')?.appendChild(shareButton);
containerDiv.querySelector('#replyButton')?.appendChild(replyButton);
if (!("image_data" in post && post.image_data)) {
// containerDiv.appendChild(timestampDiv);
return containerDiv;
// return null;
let hasImage = ("image_data" in post && post.image_data);
if (hasImage) {
let image = document.createElement("img");
image.title = `${(post.image_data!.byteLength / 1024 / 1024).toFixed(2)}MBytes`;
// const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' });
const blob = new Blob([post.image_data as ArrayBuffer]);
const url = URL.createObjectURL(blob);
image.onload = () => {
// URL.revokeObjectURL(url);
};
image.src = url;
// image.src = image.src = "data:image/png;base64," + post.image;
image.className = "postImage";
// image.onclick = () => { App.maximizeElement(image) };
containerDiv.querySelector('#image')?.appendChild(image);
}
let image = document.createElement("img");
image.title = `${(post.image_data.byteLength / 1024 / 1024).toFixed(2)}MBytes`;
// const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' });
const blob = new Blob([post.image_data as ArrayBuffer]);
const url = URL.createObjectURL(blob);
image.onload = () => {
// URL.revokeObjectURL(url);
};
image.src = url;
// image.src = image.src = "data:image/png;base64," + post.image;
image.className = "postImage";
// image.onclick = () => { App.maximizeElement(image) };
containerDiv.appendChild(image);
// containerDiv.appendChild(timestampDiv);
return containerDiv;
}

View File

@@ -6,7 +6,6 @@
import { generateID } from "IDUtils";
import { log, logID } from "log";
// import { App } from "./App";
// Use a broadcast channel to only have one peer manager for multiple tabs,
// then we won't need to have a session ID as all queries for a peerID will be coming from the same peer manager
@@ -22,7 +21,6 @@ export class PeerManager {
routingTable: Map<string, string>;
peers: Map<string, PeerConnection>;
// private signaler: Signaler;
searchQueryFunctions: Map<string, Function> = new Map();
RPC_remote: Map<string, Function> = new Map();
rpc: { [key: string]: Function } = {};
@@ -46,14 +44,6 @@ export class PeerManager {
reconnectTimer: number | null = null;
peerStateSuperlog: boolean = true;
// async watchdog() {
// // Check that we're connected to at least N peers. If not, reconnect to the bootstrap server.
// if (this.peers.size === 0) {
// await this.sendHello2();
// }
// }
animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal'];
adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy'];
snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait']
@@ -108,6 +98,18 @@ export class PeerManager {
}
onPeerMessageFromPeer(remotePeerID: string, messageJSON: any) {
let targetPeer = this.peers.get(messageJSON.to);
if (!targetPeer) {
console.log.apply(null, log("[PeerManager] Coulnd't find peer for onPeerMessageFromPeer:", messageJSON.to));
return;
}
targetPeer.send(messageJSON);
}
onWebsocketMessage(event: MessageEvent) {
let messageJSON = event.data;
let message: any = null;
@@ -149,10 +151,11 @@ export class PeerManager {
if (!peerConnection) {
let remotePeerID = message.from;
let newPeer = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
if (this._isBootstrapPeer) {
newPeer.setPolite(false);
}
newPeer.setPoliteFromID(remotePeerID, true);
peerConnection = newPeer;
this.peers.set(newPeer.remotePeerID, newPeer);
this.onConnectRequest(newPeer);
@@ -580,6 +583,12 @@ class PeerConnection {
this.polite = polite;
}
setPoliteFromID(peerID: PeerID, remote = false) {
let polite = (parseInt(peerID.charAt(peerID.length-1), 16) % 2 == 0) && !remote;
this.setPolite(polite);
}
setupDataChannel() {
if (!this.dataChannel) {
throw new Error();
@@ -713,6 +722,7 @@ class PeerConnection {
try {
this.makingOffer = true;
this.setPoliteFromID(this.peerManager.peerID);
await this.rtcPeer.setLocalDescription();
if (!this.rtcPeer.localDescription) {
@@ -801,12 +811,6 @@ class PeerConnection {
} catch (err) {
console.error(err);
}
// };
// */
}
disconnect() {
@@ -884,15 +888,12 @@ class PeerConnection {
}
}
call(functionName: string, args: any) {
let transactionID = generateID(); // make this faster as we will only ever have a small number of in-flight queries on a peer
// Think about a timeout here to auto reject it after a while.
let promise = new Promise((resolve, reject) => {
this.pendingRPCs.set(transactionID, { resolve, reject, functionName });
setTimeout(() => reject(`function:${functionName}[${transactionID}] failed to resolve after 10 seconds.`), 10_000);
let timeoutSeconds = 10;
setTimeout(() => reject(`function:${functionName}[${transactionID}] failed to resolve after ${timeoutSeconds} seconds.`), timeoutSeconds * 1000);
});
let message = {

View File

@@ -123,7 +123,8 @@ export class Sync {
'194696a2-d850-4bb0-98f7-47416b3d1662',
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
'dd1d92aa-aa24-4166-a925-94ba072a9048',
'290dbb4f-6ce1-491a-b90d-51d8efcd3d60'
'290dbb4f-6ce1-491a-b90d-51d8efcd3d60',
'3b32d0eb-94d5-49c4-a43b-0959a9fbb015'
]);
getFollowing(userID: string): string[] {

View File

@@ -15,7 +15,7 @@ const followingStoreName: string = "following"
const profileStoreName: string = "profiles"
let keyBase = "dandelion_posts_v1_"
let key = "";
let version = 1;
let version = 3;
interface IDBRequestEvent<T = any> extends Event {
@@ -28,13 +28,22 @@ type DBError = Event & {
};
async function upgrade_0to1(db: IDBDatabase) {
async function upgrade_0to1(db: IDBDatabase, _transaction: IDBTransaction) {
let postsStore = db.createObjectStore(postStoreName, { keyPath: "id", autoIncrement: true });
postsStore.createIndex("datetimeIndex", "post_timestamp", { unique: false });
postsStore.createIndex("postIDIndex", "data.post_id", { unique: true });
}
async function upgrade_1to2(_db: IDBDatabase, _transaction: IDBTransaction) {
// Was broken for some clients — index creation moved to upgrade_2to3
}
async function upgrade_2to3(_db: IDBDatabase, transaction: IDBTransaction) {
const postsStore = transaction.objectStore(postStoreName);
if (!postsStore.indexNames.contains("postReplyIndex")) {
postsStore.createIndex("postReplyIndex", "data.reply_to_id", { unique: false });
}
}
// let following = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
@@ -51,13 +60,13 @@ async function upgrade_0to1(db: IDBDatabase) {
// let tombstones = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
async function upgrade_1to2(db: IDBDatabase) {
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
let profileStore = db.createObjectStore(profileStoreName, { keyPath: "id", autoIncrement: true });
let tombstoneStore = db.createObjectStore(tombstoneStoreName, { keyPath: "id", autoIncrement: true });
tombstoneStore.createIndex("postIDIndex", "data.post_id", { unique: true });
// async function upgrade_1to2(db: IDBDatabase) {
// let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
// let profileStore = db.createObjectStore(profileStoreName, { keyPath: "id", autoIncrement: true });
// let tombstoneStore = db.createObjectStore(tombstoneStoreName, { keyPath: "id", autoIncrement: true });
// tombstoneStore.createIndex("postIDIndex", "data.post_id", { unique: true });
}
// }
// async function upgrade_1to2(db: IDBDatabase) {
// console.log("Upgrading database from 1 to 2");
@@ -86,15 +95,15 @@ async function upgrade_1to2(db: IDBDatabase) {
// }
// }
async function upgrade_2to3(db: IDBDatabase) {
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
}
// async function upgrade_2to3(db: IDBDatabase) {
// let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
// }
let upgrades = new Map([
[0, upgrade_0to1],
[1, upgrade_1to2],
[2, upgrade_2to3]
[2, upgrade_2to3],
]);
export function openDatabase(userID: string): Promise<IDBDatabase> {
@@ -110,13 +119,15 @@ export function openDatabase(userID: string): Promise<IDBDatabase> {
request.onupgradeneeded = async (event: IDBVersionChangeEvent) => {
const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
const transaction: IDBTransaction = (event.target as IDBOpenDBRequest).transaction!;
let upgradeFunction = upgrades.get(event.oldVersion);
if (!upgradeFunction) {
throw new Error(`db: Don't have an upgrade function to go from version ${event.oldVersion} to version ${event.newVersion}`);
for (let v = event.oldVersion; v < (event.newVersion ?? version); v++) {
const upgradeFunction = upgrades.get(v);
if (!upgradeFunction) {
throw new Error(`db: Don't have an upgrade function to go from version ${v} to version ${v + 1}`);
}
await upgradeFunction(db, transaction);
}
// debugger;
await upgradeFunction(db);
};
request.onsuccess = (event: Event) => {