diff --git a/src/App.ts b/src/App.ts
index 7f066a2..63db184 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -242,6 +242,7 @@ export class App {
}
console.log.apply(null, log(`[app] calling getPostIDsForUser for user [${logID(userID)}] on peer [${logID(sendingPeerID)}]`));
+ this.statusBar.updatePeerStatus(sendingPeerID, `getPostIDs(${logID(userID)})⬆️`);
let postIDs = await this.peerManager?.rpc.getPostIDsForUser(sendingPeerID, userID);
diff --git a/src/bootstrap_main.ts b/src/bootstrap_main.ts
deleted file mode 100644
index d584adb..0000000
--- a/src/bootstrap_main.ts
+++ /dev/null
@@ -1,2157 +0,0 @@
-// Attempt to get bootstrap peer to run under Deno
-// Check if RTCPeerConnection is supported.
-
-
-
-
-// TODO: virtual list, only rerender what's needed so things can keep playing.
-
-
-
-/*
-Problems
- 1. Can't delete, very annoying
- Tombstones. Send all IDs and all Tombstones. Ask only for posts that we don't get a tombstone for. Don't send posts we have a tombstone for?
-
- Posts don't propagate, you need to refresh to see new posts.
- Broadcast when we post to all peers we know about.
-
- 3. Posting is slow because too long to render
- 2. Can't follow people
- 4. Can't like or reply to posts
-
-user
- posts
- media
- tombstones
- following
- profile
- name
- description
- profile pic
-
-
-Restruucture the app around the data. App/WS split is messy. Clean it up.
-
-*/
-
-
-// import * as ForceGraph3D from "3d-force-graph";
-// import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds } from "db";
-import { generateID } from "./IDUtils.js";
-import { PeerManager } from "./PeerManager.js";
-
-import {log, logID, renderLog, setLogVisibility} from "./log.js"
-
-// import {PeerConnection} from "webRTC";
-
-// declare let WebTorrent: any;
-
-// declare let ForceGraph3D: any;
-// declare let marked: any;
-// declare let QRCode: any;
-// let posts:any;
-// let keyBase = "dandelion_posts_v1_"
-// let key:string = "";
-
-// interface PostTimestamp {
-// year: number,
-// month: number,
-// day: number,
-// hour: number,
-// minute: number,
-// second: number,
-// }
-
-// function waitMs(durationMs: number) {
-// return new Promise(resolve => setTimeout(resolve, durationMs));
-// }
-
-// function uuidToBytes(uuid: string): Uint8Array {
-// return new Uint8Array(uuid.match(/[a-fA-F0-9]{2}/g)!.map((hex) => parseInt(hex, 16)));
-// }
-
-// // Base58 character set
-// const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
-// // Base58 encoding
-// // Base58 encoding
-// function encodeBase58(buffer: Uint8Array): string {
-// let carry;
-// const digits = [0];
-
-// for (const byte of buffer) {
-// carry = byte;
-// for (let i = 0; i < digits.length; i++) {
-// carry += digits[i] << 8;
-// digits[i] = carry % 58;
-// carry = Math.floor(carry / 58);
-// }
-// while (carry > 0) {
-// digits.push(carry % 58);
-// carry = Math.floor(carry / 58);
-// }
-// }
-
-// let result = '';
-// for (const digit of digits.reverse()) {
-// result += BASE58_ALPHABET[digit];
-// }
-
-// // Handle leading zero bytes
-// for (const byte of buffer) {
-// if (byte === 0x00) {
-// result = BASE58_ALPHABET[0] + result;
-// } else {
-// break;
-// }
-// }
-
-// return result;
-// }
-
-// // Convert UUID v4 to Base58
-// function uuidToBase58(uuid: string): string {
-// const bytes = uuidToBytes(uuid);
-// return encodeBase58(bytes);
-// }
-
-// // function log(message:string) {
-// // console.log.apply(null, log(message);
-// // let log = document.getElementById("log");
-// // let newlog = document.createElement('span');
-// // newlog.innerHTML = `
${message}`;
-// // log?.appendChild(newlog);
-
-// // }
-
-
-
-
-
-// interface StoragePost {
-// data: Post;
-// }
-
-// class Post {
-// post_timestamp: Date;
-// post_id: string;
-// author: string;
-// author_id: string;
-// text: string;
-// image_data: ArrayBuffer | null;
-
-
-// importedFrom: "twitter" | null;
-// importSource: any;
-
-// constructor(
-// author: string,
-// author_id: string,
-// text: string,
-// post_timestamp: Date,
-// imageData: ArrayBuffer | null = null,
-// importedFrom: "twitter" | null = null,
-// importSource: any = null) {
-
-// this.post_timestamp = post_timestamp;
-// this.post_id = generateID();
-
-// this.author = author;
-// this.author_id = author_id;
-// this.text = text;
-// this.image_data = imageData;
-
-// this.importedFrom = importedFrom;
-// this.importSource = importSource;
-// }
-// }
-
-// globalThis.addEventListener('scroll', () => {
-// // Total height of the document
-// const totalPageHeight = document.body.scrollHeight;
-
-// // Current scroll position
-// const scrollPoint = globalThis.scrollY + globalThis.innerHeight;
-
-// // Check if scrolled to bottom
-// if (scrollPoint >= totalPageHeight) {
-// // console.log.apply(null, log('Scrolled to the bottom!'));
-// // console.log.apply(null, log(scrollPoint, totalPageHeight));
-// }
-
-// });
-
-
-
-// // let peer = await new PeerConnection(peer_id);
-
-// // let connectionReply = await wsConnection.send('hello');
-// // for (let peer of connectionReply) {
-// // let peerConnection = await wsConnection.send('connect', peer.id);
-// // if (peerConnection) {
-// // this.peers.push(peerConnection);
-// // let postIDs = await peerConnection.getPostIDs();
-// // let postsWeDontHave = this.diffPostIDs(postIDs);
-
-// // let newPosts = await peerConnection.getPosts(postsWeDontHave);
-
-// // this.addPosts(newPosts);
-
-// // }
-// // }
-
-// async function bytesToBase64DataUrl(bytes: Uint8Array, type = "application/octet-stream") {
-// return await new Promise((resolve, reject) => {
-// const reader = Object.assign(new FileReader(), {
-// onload: () => resolve(reader.result),
-// onerror: () => reject(reader.error),
-// });
-// reader.readAsDataURL(new File([bytes], "", { type }));
-// });
-// }
-
-// async function arrayBufferToBase64(buffer: ArrayBuffer) {
-// var bytes = new Uint8Array(buffer);
-// return (await bytesToBase64DataUrl(bytes) as string).replace("data:application/octet-stream;base64,", "");
-// }
-
-// async function base64ToArrayBuffer(base64String: string) {
-// let response = await fetch("data:application/octet-stream;base64," + base64String);
-// let arrayBuffer = await response.arrayBuffer();
-// return arrayBuffer;
-// }
-
-// async function compressString(input: string) {
-// // Convert the string to a Uint8Array
-// const textEncoder = new TextEncoder();
-// const inputArray = textEncoder.encode(input);
-
-// // Create a CompressionStream
-// const compressionStream = new CompressionStream('gzip');
-// const writer = compressionStream.writable.getWriter();
-
-// // Write the data and close the stream
-// writer.write(inputArray);
-// writer.close();
-
-// // Read the compressed data from the stream
-// const compressedArray = await new Response(compressionStream.readable).arrayBuffer();
-
-// // Convert the compressed data to a Uint8Array
-// return new Uint8Array(compressedArray);
-// }
-
-// interface PeerMessage {
-// type: string;
-// from: string;
-// to: string;
-// from_peername: string;
-// from_username: string;
-// message: any;
-// }
-
-// // class Signaler {
-// // websocket: WebSocket | null = null;
-
-// // websocketPingInterval: number = 0;
-
-
-
-// // connect() {
-// // if (this.websocket?.readyState === WebSocket.OPEN) {
-// // return;
-// // }
-
-// // globalThis.clearInterval(this.websocketPingInterval);
-// // if (this.websocket) { this.websocket.close() };
-
-// // try {
-// // this.websocket = new WebSocket(`wss://${globalThis.location.hostname}:${globalThis.location.port}/ws`);
-// // } catch (error: any) {
-// // console.log.apply(null, log(error.message);
-// // return;
-// // }
-
-// // this.websocket.onopen = async (event) => {
-// // console.log.apply(null, log("ws:connected"));;
-// // await this.sendHello();
-
-// // // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have.
-// // let helloRefreshIntervalPeriod = 120;
-// // if (app.isHeadless) {
-// // console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod)
-// // this.helloRefreshInterval = globalThis.setInterval(() => {
-// // console.log.apply(null, log("wsConnection: Hello refresh.")
-
-// // if (!navigator.onLine) {
-// // return;
-// // }
-// // this.sendHello();
-// // }, helloRefreshIntervalPeriod * 1000);
-// // }
-
-// // this.websocketPingInterval = globalThis.setInterval(() => {
-// // if (!navigator.onLine) {
-// // return;
-// // }
-// // this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username });
-// // }, 10_000)
-// // };
-
-// // this.websocket.onclose = (event) => {
-// // console.log.apply(null, log("ws:disconnected"));;
-// // // this.retry *= 2;
-// // console.log.apply(null, log(`Retrying in ${this.retry} seconds`));;
-// // globalThis.setTimeout(() => { this.connect(); }, this.retry * 1000);
-// // };
-
-// // this.websocket.onmessage = (event) => {
-// // // log('ws:<-' + event.data.slice(0, 240));
-// // let data = JSON.parse(event.data);
-
-// // let { type } = data;
-
-// // let handler = this.messageHandlers.get(type);
-// // if (!handler) {
-// // console.warn(`Got a message we can't handle:`, type);
-// // return;
-// // }
-
-// // handler(data);
-
-// // };
-
-// // this.websocket.onerror = (event) => {
-// // console.log.apply(null, log('ws:error: ' + event));;
-// // };
-// // }
-// // }
-
-// // disconnect() {
-// // this.websocket?.close();
-// // }
-// // }
-
-
-// // Connect websocket
-// // send hello
-// // get bootstrap peer ID
-// // WebRTC connect to bootstrap peer
-// // ask Bootstrap peer for peers that have users we care about.
-// // for now, bootstrap peer will connect to all peers and will tell us about them, moving all logic from the server to the BSP
-// // WebRTC Connect to peers that might have posts we need
-// // query those peers and do existing logic.
-
-// class wsConnection {
-// websocket: WebSocket | null = null;
-// sessionID = "";
-// userID = "";
-// peerID = "";
-// rtcPeerDescription: RTCSessionDescription | null = null;
-// UserIDsToSync: Set;
-// websocketPingInterval: number = 0;
-// helloRefreshInterval: number = 0;
-// retry = 10;
-// state = 'disconnected';
-// // peers: Map = new Map();
-
-// messageHandlers: Map void> = new Map();
-// peerMessageHandlers: Map void> = new Map();
-// seenPeers: Map = new Map();
-
-
-// constructor(userID: string, peerID: string, IDsToSync: Set, rtcPeerDescription: RTCSessionDescription) {
-// this.rtcPeerDescription = rtcPeerDescription;
-// this.sessionID = generateID();
-// this.userID = userID;
-// this.peerID = peerID;
-// this.UserIDsToSync = new Set(IDsToSync);
-
-// this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
-// this.messageHandlers.set('hello2', this.hello2ResponseHandler.bind(this));
-// this.messageHandlers.set('pong', this.pongHandler);
-// this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
-
-
-// //
-// this.peerMessageHandlers.set('get_post_ids_for_user', this.getPostIdsForUserHandler.bind(this));
-// this.peerMessageHandlers.set('get_post_ids_for_user_response', this.getPostIdsForUserResponseHandler.bind(this));
-
-// this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
-// this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserReponseHandler.bind(this));
-
-// // this.peerMessageHandlers.set('send_webrtc_offer', this.sendWebRTCOfferHandler.bind(this));
-// // this.peerMessageHandlers.set('send_webrtc_offer_response', this.getPostIdsForUserResponseHandler.bind(this));
-
-
-
-// globalThis.addEventListener('beforeunload', () => this.disconnect());
-
-// this.connect();
-// }
-
-// // So we don't need custom logic everywhere we use this, I just wrapped it.
-// shouldSyncUserID(userID: string) {
-// if (app.isHeadless) {
-// return true;
-// }
-
-// return this.UserIDsToSync.has(userID);
-// }
-
-// async send(message: any) {
-// let json = ""
-// try {
-// json = JSON.stringify(message);
-// // console.log.apply(null, log("*******", (await compressString(json)).byteLength, json.length);
-// } catch (e) {
-// console.log.apply(null, log(e, "wsConnection send: Couldn't serialize message", message));
-// }
-// // log(`ws->${json.slice(0, 240)}`)
-// this.websocket!.send(json);
-
-// }
-
-// pongHandler(data: any) {
-// }
-
-
-// async sendWebRTCDescription(description: RTCSessionDescription | null) {
-
-// console.log.apply(null, log("description:", description));
-// this.send({ type: "rtc_session_description", description: description });
-// }
-
-// async getPostIdsForUserResponseHandler(data: any) {
-// // log(`getPostsForUserResponse: ${data}`)
-
-// let message = data.message;
-// console.log.apply(null, log(`Net: got ${message.post_ids.length} post IDs for user ${logID(message.user_id)} from peer ${logID(data.from)}`));;
-
-
-// let startTime = app.timerStart();
-// let postIds = await checkPostIds(message.user_id, message.post_ids);
-// console.log.apply(null, log(`ID Check for user ${logID(message.user_id)} took ${app.timerDelta().toFixed(2)}ms`));;
-// console.log.apply(null, log(`Need ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`));;
-
-// if (postIds.length === 0) {
-// return;
-// }
-
-// console.log.apply(null, log(`Net: Req ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`));
-// let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user", post_ids: postIds, user_id: message.user_id } }
-
-// this.send(responseMessage);
-// }
-
-
-
-// // static async compressArrayBuffer(data: ArrayBuffer): Promise {
-// // const compressionStream = new CompressionStream('gzip'); // You can also use 'deflate', 'deflate-raw', etc.
-
-// // const compressedStream = new Response(
-// // new Blob([data]).stream().pipeThrough(compressionStream)
-// // );
-
-// // const compressedArrayBuffer = await compressedStream.arrayBuffer();
-
-// // return compressedArrayBuffer;
-// // }
-// postBlockList = new Set([
-// '1c71f53c-c467-48e4-bc8c-39005b37c0d5',
-// '64203497-f77b-40d6-9e76-34d17372e72a',
-// '243130d8-4a41-471e-8898-5075f1bd7aec',
-// 'e01eff89-5100-4b35-af4c-1c1bcb007dd0',
-// '194696a2-d850-4bb0-98f7-47416b3d1662',
-// 'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
-// 'dd1d92aa-aa24-4166-a925-94ba072a9048'
-// ]);
-
-// async getPostIdsForUserHandler(data: any) {
-// let message = data.message;
-// let postIds = await getAllIds(message.user_id) ?? [];
-// postIds = postIds.filter((postID: string) => !this.postBlockList.has(postID));
-// if (postIds.length === 0) {
-// console.log.apply(null, log(`Net: I know about user ${logID(message.user_id)} but I have 0 posts, so I'm not sending any to to peer ${logID(data.from)}`));;
-// return;
-// }
-// console.log.apply(null, log(`Net: Sending ${postIds.length} post Ids for user ${logID(message.user_id)} to peer ${logID(data.from)}`));
-
-// let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_post_ids_for_user_response", post_ids: postIds, user_id: message.user_id } }
-// this.send(responseMessage);
-// }
-
-// async broadcastNewPost(userID: string, post: any) {
-
-// let newPost = { ...post }
-// if (post.image_data) {
-// newPost.image_data = await arrayBufferToBase64(post.image_data);
-// }
-
-// for (let [peerID, peerInfo] of this.seenPeers.entries()) {
-// console.log.apply(null, log(`broadcastNewPost: sending new post to ${logID(peerID)}:${peerInfo.peerName}:${peerInfo.userName}`));;
-
-// this.sendPostsForUser(peerID, app.userID, [newPost])
-// }
-// }
-
-
-// async sendPostsForUser(toPeerID: string, userID: string, posts: any) {
-// let responseMessage = {
-// type: "peer_message",
-// from: app.peerID,
-// to: toPeerID,
-// from_username: app.username,
-// from_peername: app.peername,
-// message: {
-// type: "get_posts_for_user_response",
-// posts: posts,
-// user_id: userID
-// }
-// }
-
-// return this.send(responseMessage);
-
-// }
-
-// // Send posts to peer
-// async getPostsForUserHandler(data: any) {
-// let message = data.message;
-// let posts = await getPostsByIds(message.user_id, message.post_ids) ?? [];
-
-// console.log.apply(null, log(`Net: Sending ${posts.length} posts for user ${logID(message.user_id)} to peer ${logID(data.from)}`));;
-
-// app.timerStart();
-// let output = [];
-
-// console.log.apply(null, log("Serializing images"));
-// for (let post of posts) {
-// let newPost = (post as any).data;
-
-// if (newPost.image_data) {
-// // let compressedData = await wsConnection.compressArrayBuffer(newPost.image_data);
-// // console.log.apply(null, log((newPost.image_data.byteLength - compressedData.byteLength) / 1024 / 1024);
-
-// // TODO don't do this, use Blobs direclty!
-// // https://developer.chrome.com/blog/blob-support-for-Indexeddb-landed-on-chrome-dev
-
-// newPost.image_data = await arrayBufferToBase64(newPost.image_data);
-
-// }
-
-// // let megs = JSON.stringify(newPost).length/1024/1024;
-// // console.log.apply(null, log(`getPostsForUserHandler id:${newPost.post_id} post length:${megs}`);
-// output.push(newPost);
-// }
-
-// let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user_response", posts: output, user_id: message.user_id } }
-
-// console.log.apply(null, log("Sending posts"));
-// await this.sendPostsForUser(data.from, message.user_id, output);
-// let sendTime = app.timerDelta();
-// console.log.apply(null, log(`getPostsForUserHandler send took: ${sendTime.toFixed(2)}ms`));;
-
-// }
-
-
-
-// // Got posts from peer
-// async getPostsForUserReponseHandler(data: any) {
-// app.timerStart();
-// let message = data.message;
-// console.log.apply(null, log(`Net: got ${message.posts.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`));
-// for (let post of message.posts) {
-
-// // HACK: Some posts have insanely large images, so I'm gonna skip them.
-// // Once we support delete then we we could delete these posts in a sensible way.
-// if (this.postBlockList.has(post.post_id)) {
-// console.log.apply(null, log(`Skipping blocked post: ${post.post_id}`));;
-// continue;
-// }
-
-// // HACK - some posts had the wrong author ID
-// if (message.user_id === app.userID) {
-// post.author_id = app.userID;
-// }
-
-// post.post_timestamp = new Date(post.post_timestamp);
-// if (post.image_data) {
-// post.image_data = await base64ToArrayBuffer(post.image_data);
-// }
-// }
-// console.log.apply(null, log(`Merging same user peer posts...`));
-// await mergeDataArray(message.user_id, data.message.posts);
-
-// let receiveTime = app.timerDelta();
-
-// console.log.apply(null, log(`getPostsForUserReponseHandler receive took: ${receiveTime.toFixed(2)}ms`));;
-
-
-// if (message.user_id === app.getPreferentialUserID() || app.following.has(message.user_id)) {
-// app.render();
-// }
-// }
-
-
-
-
-// async peerMessageHandler(data: PeerMessage) {
-// // log(`peerMessageHandler ${JSON.stringify(data)}`)
-
-// this.seenPeers.set(data.from, { peerName: data.from_peername, userName: data.from_username });
-
-// let peerMessageType = data.message.type;
-
-// let handler = this.peerMessageHandlers.get(peerMessageType);
-
-// if (!handler) {
-// console.error(`got peer message type we don't have a handler for: ${peerMessageType}`);
-// return;
-// }
-
-// handler(data);
-// }
-
-// userBlockList = new Set([
-// '5d63f0b2-a842-41bf-bf06-e0e4f6369271',
-// '5f1b85c4-b14c-454c-8df1-2cacc93f8a77',
-// // 'bba3ad24-9181-4e22-90c8-c265c80873ea'
-// ])
-
-
-// // Hello2
-// // Goal, connect to bootstrap peer, ask bootstrap peer for peers that have posts from users that we care about. get peers, connect to those peers, sync.
-// // how? do "perfect negotiation" with bootstrap peer. All logic here moves to BP.
-
-// async sendHello2() {
-// this.send({
-// type: "hello2",
-// user_id: this.userID,
-// user_name: app.username,
-// peer_id: this.peerID,
-// session_id: this.sessionID,
-// peer_name: app.peername,
-// is_bootstrap_peer: app.isBootstrapPeer,
-// peer_description: this.rtcPeerDescription
-// });
-// }
-
-// async sendHello() {
-// // TODO only get users you're following here. ✅
-// let knownUsers = [...(await indexedDB.databases())].map(db => db.name?.replace('user_', '')).filter(userID => userID !== undefined);
-// knownUsers = knownUsers
-// .filter(userID => this.shouldSyncUserID(userID))
-// .filter(userID => !this.userBlockList.has(userID))
-// .filter(async userID => (await getAllIds(userID)).length > 0); // TODO:EASYOPT getting all the IDs is unecessary, replace it with a test to get a single ID.
-
-// console.log.apply(null, log('Net: Sending known users', knownUsers.map(userID => logID(userID ?? ""))));
-// return await this.send({ type: "hello", user_id: this.userID, user_name: app.username, peer_id: this.peerID, peer_name: app.peername, known_users: knownUsers });
-// }
-
-// hello2ResponseHandler(data: any) {
-
-// }
-
-// helloResponseHandler(data: any) {
-
-// let users = [];
-// let receivedUsers = Object.entries(data.userPeers);
-// console.log.apply(null, log(`Net: got ${receivedUsers.length} users from bootstrap peer.`));
-
-// try {
-// let preferentialUserID = app.getPreferentialUserID();
-// let currentUserPeers = data.userPeers[preferentialUserID];
-// users.push([preferentialUserID, currentUserPeers]);
-// delete data.userPeers[preferentialUserID];
-// } catch (e) {
-// console.log.apply(null, log('helloResponseHandler', e));
-// }
-
-// let getAllUsers = app.router.route !== App.Route.USER
-// if (getAllUsers) {
-// users = [...users, ...Object.entries(data.userPeers).filter(userID => this.shouldSyncUserID(userID[0]))];
-// }
-
-// // log(`Net: got ${users.length} users from bootstrap peer. \n${users.map((user)=>user[0]).join('\n')}`)
-
-// for (let [userID, peerIDs] of users) {
-// if (this.userBlockList.has(userID)) {
-// console.log.apply(null, log("Skipping user on blocklist:", userID));
-// continue;
-// }
-
-// // this.peers.set(userID, [...peerIDs]);
-
-// for (let peerID of [...peerIDs]) {
-// if (peerID === this.peerID) {
-// continue;
-// }
-
-// console.log.apply(null, log(`Net: Req post IDs for user ${logID(userID)} from peer ${logID(peerID)}`));;
-// this.send({
-// type: "peer_message",
-// from: this.peerID,
-// from_username: app.username,
-// from_peername: app.peername,
-// to: peerID,
-// message: { type: "get_post_ids_for_user", user_id: userID }
-// })
-// }
-// }
-// }
-
-// connect(): void {
-// if (this.websocket?.readyState === WebSocket.OPEN) {
-// return;
-// }
-
-// globalThis.clearInterval(this.websocketPingInterval);
-// if (this.websocket) { this.websocket.close() };
-
-// try {
-// this.websocket = new WebSocket(`wss://${globalThis.location.hostname}:${globalThis.location.port}/ws`);
-// } catch (error: any) {
-// console.log.apply(null, log(error.message));
-// return;
-// }
-
-// this.websocket.onopen = async (event) => {
-// console.log.apply(null, log("ws:connected"));;
-// await this.sendHello2();
-
-// // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have.
-// // let helloRefreshIntervalPeriod = 120;
-// // if (app.isHeadless) {
-// // console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod)
-// // this.helloRefreshInterval = globalThis.setInterval(() => {
-// // console.log.apply(null, log("wsConnection: Hello refresh.")
-
-// // if (!navigator.onLine) {
-// // return;
-// // }
-// // this.sendHello();
-// // }, helloRefreshIntervalPeriod * 1000);
-// // }
-
-// this.websocketPingInterval = globalThis.setInterval(() => {
-// if (!navigator.onLine) {
-// return;
-// }
-// this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username });
-// }, 10_000)
-// };
-
-// // this.websocket.onopen = async (event) => {
-// // console.log.apply(null, log("ws:connected"));;
-// // await this.sendHello();
-
-// // // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have.
-// // let helloRefreshIntervalPeriod = 120;
-// // if (app.isHeadless) {
-// // console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod)
-// // this.helloRefreshInterval = globalThis.setInterval(() => {
-// // console.log.apply(null, log("wsConnection: Hello refresh.")
-
-// // if (!navigator.onLine) {
-// // return;
-// // }
-// // this.sendHello();
-// // }, helloRefreshIntervalPeriod * 1000);
-// // }
-
-// // this.websocketPingInterval = globalThis.setInterval(() => {
-// // if (!navigator.onLine) {
-// // return;
-// // }
-// // this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username });
-// // }, 10_000)
-// // };
-
-// this.websocket.onclose = (event) => {
-// console.log.apply(null, log("ws:disconnected"));;
-// // this.retry *= 2;
-// console.log.apply(null, log(`Retrying in ${this.retry} seconds`));;
-// globalThis.setTimeout(() => { this.connect(); }, this.retry * 1000);
-// };
-
-// this.websocket.onmessage = (event) => {
-// // log('ws:<-' + event.data.slice(0, 240));
-// let data = JSON.parse(event.data);
-
-// let { type } = data;
-
-// let handler = this.messageHandlers.get(type);
-// if (!handler) {
-// console.warn(`Got a message we can't handle:`, type);
-// return;
-// }
-
-// handler(data);
-
-// };
-
-// this.websocket.onerror = (event) => {
-// console.log.apply(null, log('ws:error: ' + event));;
-// };
-// }
-
-// disconnect() {
-// this.websocket?.close();
-// }
-// }
-
-class App {
- username: string = '';
- peername: string = '';
- userID: string = '';
- peerID: string = '';
- following: Set = new Set();
-// posts: StoragePost[] = [];
- isHeadless: boolean = false;
- isBootstrapPeer: boolean = false;
- showLog: boolean = false;
- markedAvailable = false;
- limitPosts = 50;
-// websocket: wsConnection | null = null;
- // vizGraph: any | null = null;
- qrcode: any = null;
- connectURL: string = "";
- firstRun = false;
- peerManager: PeerManager | null = null;
-
- async connect() {
- this.peerManager = new PeerManager(this.userID, this.peerID, this.isBootstrapPeer);
- this.registerRPCs();
- console.log.apply(null, log("*************** before peerManager.connect"));;
-
- // We use promises here to only return from this call once we're connected to the boostrap peer
- // and the datachannel is open.
- // Might want to take this a step further and only return once we're connected to an initial set of peers?
- // we could return progress information as we connect and have the app subscribe to that?
-
- // Would be lovely to show a little display of peers connecting, whether you're connected directly to a friend's peer etc.
- // Basically that live "dandelion" display.
-
- this.peerManager.registerRPC('getPostIDsForUser', (userID: any) => {
- return [1, 2, 3, 4, 5]
- });
-
- await this.peerManager.connect();
- console.log.apply(null, log("*************** after peerManager.connect"));;
-
-
- if (!this.isBootstrapPeer) {
- let postIDs = await this.peerManager.rpc.getPostIDsForUser(this.peerManager.bootstrapPeerID, this.userID);
- console.log.apply(null, log("peerManager.rpc.getPostIDsForUser", postIDs));
- }
-
-
-
- }
-
-// getPreferentialUserID() {
-// return this.router.userID.length !== 0 ? this.router.userID : this.userID;
-// }
-
-// initMarkdown() {
-// if (typeof marked === "undefined") {
-// return;
-// }
-
-// const renderer = new marked.Renderer();
-// renderer.link = (href: any, title: string, text: string) => {
-// return `${text}`;
-// };
-// marked.setOptions({ renderer: renderer });
-
-// this.markedAvailable = true;
-// }
-
- // arrayBufferToBase64(buffer: ArrayBuffer) {
- // return new Promise((resolve, reject) => {
- // const blob = new Blob([buffer], { type: 'application/octet-stream' });
- // const reader = new FileReader();
-
- // reader.onloadend = () => {
- // const dataUrl = reader.result as string;
- // if (!dataUrl) {
- // resolve(null);
- // return;
- // }
- // const base64 = dataUrl.split(',')[1];
- // resolve(base64);
- // };
-
- // reader.onerror = (error) => {
- // reject(error);
- // };
-
- // reader.readAsDataURL(blob);
- // });
- // }
-
-// async createTestData() {
-// let postsTestData = await (await fetch("./postsTestData.json")).json();
-
-// return postsTestData;
-// }
-
-// time = 0;
-
-// timerStart() {
-// this.time = performance.now();
-// }
-
-// timerDelta() {
-// return performance.now() - this.time;
-// }
-
-// getFixedTweetText(entry: any) {
-
-
-// let fullText = entry.tweet.full_text;
-
-// let linkMarkdown = "";
-// for (const url of entry.tweet.entities.urls) {
-// linkMarkdown = `[${url.display_url}](${url.expanded_url})`;
-// fullText = fullText.replace(url.url, linkMarkdown);
-// }
-
-// return fullText
-// }
-
-// downloadBinary(data: ArrayBuffer, filename: string, mimeType: string = 'application/octet-stream') {
-// // Create a blob from the ArrayBuffer with the specified MIME type
-// const blob = new Blob([data], { type: mimeType });
-
-// // Create object URL from the blob
-// const url = globalThis.URL.createObjectURL(blob);
-
-// // Create temporary link element
-// const link = document.createElement('a');
-// link.href = url;
-// link.download = filename;
-
-// // Append link to body, click it, and remove it
-// document.body.appendChild(link);
-// link.click();
-// document.body.removeChild(link);
-
-// // Clean up the object URL
-// globalThis.URL.revokeObjectURL(url);
-// }
-
-// downloadJson(data: any, filename = 'data.json') {
-// const jsonString = JSON.stringify(data);
-// const blob = new Blob([jsonString], { type: 'application/json' });
-// const url = globalThis.URL.createObjectURL(blob);
-// const link = document.createElement('a');
-// link.href = url;
-// link.download = filename;
-// document.body.appendChild(link);
-// link.click();
-// document.body.removeChild(link);
-// globalThis.URL.revokeObjectURL(url);
-// }
-
-// async importPostsForUser(userID: string, posts: string) {
-
-// }
-
-// async exportPostsForUser(userID: string) {
-
-// let posts = await getAllData(userID);
-
-// let output = [];
-
-// console.log.apply(null, log("Serializing images"));
-// for (let post of posts) {
-// let newPost = (post as any).data;
-
-// if (newPost.image_data) {
-// newPost.image_data = await arrayBufferToBase64(newPost.image_data);
-// }
-
-// output.push(newPost);
-// }
-
-// let compressedData = await compressString(JSON.stringify(output));
-
-// const d = new Date();
-// const timestamp = `${d.getFullYear()
-// }_${String(d.getMonth() + 1).padStart(2, '0')
-// }_${String(d.getDate()).padStart(2, '0')
-// }_${String(d.getHours()).padStart(2, '0')
-// }_${String(d.getMinutes()).padStart(2, '0')
-// }_${String(d.getSeconds()).padStart(2, '0')}`;
-
-
-// this.downloadBinary(compressedData, `ddln_${this.username}_export_${timestamp}.json.gz`);
-// }
-
-// async importTweetArchive(userID: string, tweetArchive: any[]) {
-// console.log.apply(null, log("Importing tweet archive"));
-// let postsTestData: any[] = [];
-
-// // let response = await fetch("./tweets.js");
-// // let tweetsText = await response.text();
-// // tweetsText = tweetsText.replace("globalThis.YTD.tweets.part0", "globalThis.tweetData");
-
-// // new Function(tweetsText)();
-
-
-// // let tweets = JSON.parse(tweetJSON);
-// let count = 0;
-
-// for (let entry of tweetArchive) {
-// // if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) {
-// // continue;
-// // }
-
-// let mediaURL: string = entry.tweet?.entities?.media?.[0]?.media_url_https;
-// let isImage = false;
-// if (mediaURL) {
-// isImage = mediaURL.includes('jpg');
-// }
-
-// let imageData = null;
-// // if (isImage) {
-// // try {
-// // let response = await fetch(mediaURL);
-// // await waitMs(100);
-// // if (response.status === 200) {
-// // imageData = await response.arrayBuffer();
-// // }
-// // console.log.apply(null, log(imageData);
-// // } catch (e) {
-// // console.log.apply(null, log(e);
-// // }
-
-// // }
-
-// let timeStamp = new Date(entry.tweet.created_at);
-// let tweetText = this.getFixedTweetText(entry);
-// let newPost = new Post('bobbydigitales', userID, tweetText, timeStamp, imageData, 'twitter', entry);
-
-// postsTestData.push(newPost);
-
-// count++;
-// if (count % 100 === 0) {
-// console.log.apply(null, log(`Imported ${count} posts...`));;
-// // render(postsTestData);
-// }
-
-// // if (count == 100-1) {
-// // break;
-// // }
-
-// }
-// return postsTestData;
-// }
-
-// async createTestData3(userID: string) {
-// let posts = await (await (fetch('./posts.json'))).json();
-
-// return posts;
-// }
-
-// async registerServiceWorker() {
-// if (!("serviceWorker" in navigator)) {
-// return;
-// }
-
-// let registrations = await navigator.serviceWorker.getRegistrations();
-// if (registrations.length > 0) {
-// console.log.apply(null, log("Service worker already registered."));
-// return registrations[0];
-// }
-
-// navigator.serviceWorker
-// .register("/sw.js")
-// .then((registration) => {
-// console.log.apply(null, log("Service Worker registered with scope:", registration.scope));
-// return registration;
-// })
-// .catch((error) => {
-// console.error("Service Worker registration failed:", error);
-// });
-// }
-
-// async compressImage(imageData: ArrayBuffer, mimeType: string, quality = 0.5): Promise {
-// let uncompressedByteLength = imageData.byteLength;
-// console.log.apply(null, log(`compressImage input:${mimeType} size:${(uncompressedByteLength / 1024).toFixed(2)}KBi quality:${quality}`));;
-
-// try {
-// // Convert ArrayBuffer to Blob
-// const blob = new Blob([imageData], { type: mimeType });
-
-// const bitmap = await createImageBitmap(blob, {
-// imageOrientation: 'none',
-
-// // resizeWidth: desiredWidth,
-// // resizeHeight: desiredHeight,
-// // resizeQuality: 'high',
-// });
-
-// // const bitmap = await createImageBitmap(bitmapTemp, {
-// // imageOrientation: 'none',
-
-// // resizeWidth: 600,
-// // resizeHeight: 800,
-// // // resizeHeight: (bitmapTemp.height / bitmapTemp.width) * 600,
-// // resizeQuality: 'high',
-// // })
-
-// //drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
-
-
-// // Create a canvas and draw the image onto it
-// // let scale = 1/32;
-// // let scaledWidth = bitmap.width*scale;
-// // let scaledHeight = bitmap.height*scale;
-
-// // let scale = 1/32;
-
-// let scaledWidth = bitmap.width;
-// let scaledHeight = bitmap.height;
-
-// let resizeThreshold = 600;
-// if (scaledWidth > resizeThreshold) {
-// scaledWidth = resizeThreshold;
-// scaledHeight = (bitmap.height / bitmap.width) * resizeThreshold;
-// }
-
-// const canvas = document.createElement('canvas');
-// canvas.width = scaledWidth;
-// canvas.height = scaledHeight;
-// const ctx = canvas.getContext('2d');
-// ctx!.imageSmoothingEnabled = true;
-// ctx!.imageSmoothingQuality = 'high';
-// canvas!.getContext('2d')!.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, scaledWidth, scaledHeight);
-
-
-// // Compress the image and get the result as an ArrayBuffer
-// const compressedBlob = await new Promise((resolve, reject) =>
-// canvas.toBlob(
-// (blob) => (blob ? resolve(blob as Blob) : reject(new Error('Compression failed.'))),
-// 'image/jpeg',
-// quality
-// )
-// );
-
-
-// // TODO: Don't need to do this as we'll be storing blobs directly.
-// let compressedArrayBuffer = await (compressedBlob as Blob).arrayBuffer();
-// let compressedByteLength = compressedArrayBuffer.byteLength;
-
-// let percent = (uncompressedByteLength / compressedByteLength)
-// console.log.apply(null, log(`compressImage: compressedSize:${(compressedArrayBuffer.byteLength / 1024).toFixed(2)}KBi ${percent.toFixed(2)}:1 compression`));;
-// return compressedArrayBuffer;
-// } catch (e) {
-// console.error(e);
-// return null;
-// }
-// }
-
-// async createNewPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4") {
-// if ((typeof postText !== "string") || postText.length === 0) {
-// console.log.apply(null, log("Not posting an empty string..."));
-// return;
-// }
-
-// if (mediaData &&
-// (mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
-// (mediaData as ArrayBuffer).byteLength > 500 * 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);
-// // this.posts.push(post);
-// // localStorage.setItem(key, JSON.stringify(posts));
-// addData(userID, post);
-
-// this.websocket?.broadcastNewPost(userID, post);
-
-
-// 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);
- }
-
- 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);
- }
-
- return id;
- }
-
- 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']
-
- hashIdToIndices(id: string) {
- let indices = [];
- for (let char of id) {
- if (char !== '0' && char !== '-') {
- indices.push(parseInt(char, 16));
- if (indices.length == 2) {
- break;
- }
- }
- }
- return [indices[0], indices[1]];
- }
-
- funkyName(id: string, listOne: string[], listTwo: string[]) {
- let [one, two] = this.hashIdToIndices(id);
- let first = listOne[one % this.adjectives.length];
- let second = listTwo[two % this.animals.length];
- return { first, second }
- }
-
- getUsername() {
- let username = localStorage.getItem("dandelion_username");
-
- if (username && username !== "not_set") {
- return username;
- }
-
- let { first: adjective, second: animal } = this.funkyName(this.userID, this.adjectives, this.animals);
- username = `${adjective}_${animal}`
- localStorage.setItem("dandelion_username", username);
-
- return username;
- }
-
- getPeername() {
- let { first: adjective, second: snake } = this.funkyName(this.peerID, this.adjectives, this.snakes);
- let peername = `${adjective}_${snake}`
- return peername;
- }
-
-// setFont(fontName: string, fontSize: string) {
-
-// let content = document.getElementById('content');
-
-// if (!content) {
-// return;
-// }
-
-// content.style.fontFamily = fontName;
-// content.style.fontSize = fontSize;
-
-// let textArea = document.getElementById('textarea_post');
-// if (!textArea) {
-// return;
-// }
-
-// textArea.style.fontFamily = fontName;
-// textArea.style.fontSize = fontSize;
-// }
-
-// initOffline(connection: wsConnection) {
-// // Event listener for going offline
-// globalThis.addEventListener('offline', () => {
-// console.log.apply(null, log("offline"));
-// });
-
-// // Event listener for going online
-// globalThis.addEventListener('online', async () => {
-// console.log.apply(null, log("online"));
-// // connection.connect();
-// this.render();
-// });
-
-// console.log.apply(null, log(`Online status: ${navigator.onLine ? "online" : "offline"}`));
-
-// }
-
-// selectFile(contentType: string): Promise {
-// return new Promise(resolve => {
-// let input = document.createElement('input');
-// input.type = 'file';
-// // input.multiple = multiple;
-// input.accept = contentType;
-
-// input.onchange = () => {
-// if (input.files == null) {
-// resolve(null);
-// return;
-// }
-
-// let files = Array.from(input.files);
-
-// // if (multiple)
-// // resolve(files);
-// // else
-// resolve(files[0]);
-// };
-
-// input.click();
-// });
-// }
-
-// readFile(file: File): Promise {
-// // Always return a Promise
-// return new Promise((resolve, reject) => {
-// let content = '';
-// const reader = new FileReader();
-// // Wait till complete
-// reader.onloadend = function (e: any) {
-// content = e.target.result;
-// resolve(content);
-// };
-// // Make sure to handle error states
-// reader.onerror = function (e: any) {
-// reject(e);
-// };
-// reader.readAsText(file);
-// });
-// }
-
-// async lazyCreateQRCode() {
-// if (this.qrcode != null) {
-// return;
-// }
-// this.qrcode = await new QRCode(document.getElementById('qrcode'), {
-// text: this.connectURL,
-// width: 150,
-// height: 150,
-// colorDark: "#000000",
-// colorLight: "#ffffff",
-// correctLevel: QRCode.CorrectLevel.H
-// });
-// }
-
-// showInfo() {
-// let infoElement = document.getElementById('info');
-
-// if (infoElement === null) {
-// return;
-// }
-// infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none';
-// setLogVisibility(infoElement.style.display == 'block');
-// renderLog();
-// this.lazyCreateQRCode();
-// (document.querySelector('#qrcode > img') as HTMLImageElement).classList.add('qrcode_image');
-// (document.querySelector('#qrcode > canvas') as HTMLImageElement).classList.add('qrcode_image');
-
-// this.showLog = true;
-
-
-// }
-
-// button(elementName: string) {
-// return document.getElementById(elementName) as HTMLButtonElement;
-// }
-
-// div(elementName: string) {
-// return document.getElementById(elementName) as HTMLDivElement;
-// }
-
-// initButtons(userID: string, posts: StoragePost[], registration: ServiceWorkerRegistration | undefined) {
-// // let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
-// // let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
-// // let importTweetsButton = document.getElementById("import_tweets") as HTMLButtonElement;
-// // let toggleDark = document.getElementById('toggle_dark') as HTMLButtonElement;
-// // let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement;
-// // let updateApp = document.getElementById("update_app") as HTMLButtonElement;
-// // let ddlnLogoButton = document.getElementById('ddln_logo_button') as HTMLDivElement;
-// // let addPic = document.getElementById('button_add_pic') as HTMLDivElement;
-// // toggleDark.addEventListener('click', () => {
-// // document.documentElement.style.setProperty('--main-bg-color', 'white');
-// // document.documentElement.style.setProperty('--main-fg-color', 'black');
-// // })
-
-// let homeButton = this.div('home-button');
-// homeButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/`)
-
-// let profileButton = this.div('profile-button');
-// profileButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/user/${this.userID}`)
-
-// let monitorButton = this.div('monitor_button');
-// monitorButton.addEventListener('click', async () => {
-// navContainer.classList.toggle('active');
-// this.showInfo()
-// });
-
-// let navContainer = this.div('nav-container');
-// let burgerMenuButton = this.div('burger-menu-button');
-// burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
-
-// let exportButton = this.button("export-button");
-// exportButton.addEventListener('click', async e => {
-
-// await this.exportPostsForUser(this.userID)
-// });
-
-// let composeButton = this.div('compose-button');
-// composeButton.addEventListener('click', e => {
-// document.getElementById('compose')!.style.display = 'block';
-// document.getElementById('textarea_post')?.focus();
-// });
-
-
-// let filePicker = document.getElementById('file-input') as HTMLInputElement;
-// 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);
-// }
-
-// // Reset so that if they pick the same image again, we still get the change event.
-// filePicker.value = '';
-// });
-
-// let filePickerLabel = document.getElementById('file-input-label');
-// filePickerLabel?.addEventListener('click', () => {
-// console.log.apply(null, log("Add pic..."));
-// })
-
-
-// let usernameField = document.getElementById('username');
-// usernameField?.addEventListener('input', (event: any) => {
-// this.username = event.target.innerText;
-// localStorage.setItem("dandelion_username", this.username);
-// })
-
-// // importTweetsButton.addEventListener('click', async () => {
-// // let file = await this.selectFile('text/*');
-
-// // console.log.apply(null, log(file);
-// // if (file == null) {
-// // return;
-// // }
-
-// // let tweetData = await this.readFile(file);
-// // tweetData = tweetData.replace('globalThis.YTD.tweets.part0 = ', '');
-// // const tweets = JSON.parse(tweetData);
-
-// // let imported_posts = await this.importTweetArchive(userID, tweets);
-// // clearData(userID);
-// // // posts = posts.reverse();
-// // addDataArray(userID, imported_posts);
-// // this.render();
-
-// // });
-
-// // clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() });
-
-
-// let postButton = document.getElementById("button_post") as HTMLButtonElement;
-// let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
-
-// if (!(postButton && postText)) {
-// throw new Error();
-// }
-
-// 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);
-// });
-
-// postButton.addEventListener("click", () => {
-// this.createNewPost(userID, postText.value);
-// postText.value = "";
-// document.getElementById('compose')!.style.display = 'none';
-// });
-
-// // updateApp.addEventListener("click", () => {
-// // registration?.active?.postMessage({ type: "update_app" });
-// // });
-
-
-// // ddlnLogoButton.addEventListener('click', async () => {
-// // this.showInfo()
-// // });
-
-// }
-
-// async getPostsForFeed() {
-
-// // get N posts from each user and sort them by date.
-// // This isn't really going to work very well.
-// // Eventually we'll need a db that only has followed user posts so we can get them chronologically
-// //
-// let posts: StoragePost[] = [];
-// for (let followedID of this.following.keys()) {
-// posts = posts.concat(await getData(followedID, new Date(2022, 8), new Date()));
-// // console.log.apply(null, log(followedID);
-// }
-
-// // @ts-ignore
-// posts = posts.sort((a, b) => a.post_timestamp - b.post_timestamp);
-
-// return posts;
-// }
-
-// async loadFollowersFromStorage(userID: string): Promise {
-
-// // Rob
-// if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') {
-// return [
-// 'b38b623c-c3fa-4351-9cab-50233c99fa4e',
-// '6d774268-16cd-4e86-8bbe-847a0328893d', // Sean
-// '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
-// 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
-// 'bba3ad24-9181-4e22-90c8-c265c80873ea', // Harry
-// '8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona
-// ]
-// }
-
-// // Martin
-// if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') {
-// return [
-// 'b38b623c-c3fa-4351-9cab-50233c99fa4e',
-// 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
-// ]
-// }
-
-// // Fiona
-// if (userID === '8f6802be-c3b6-46c1-969c-5f90cbe01479') {
-// return [
-// 'b38b623c-c3fa-4351-9cab-50233c99fa4e', // Rob
-// 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
-// '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
-// ]
-// }
-
-// return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :)
-// }
-
-// async loadPostsFromStorage(userID: string, postID?: string) {
-
-// this.timerStart();
-// let posts: StoragePost[] = [];
-
-// // if (postID) {
-// // posts = await gePostForUser(userID, postID);
-// // }
-
-// posts = await getData(userID, new Date(2022, 8), new Date());
-
-// if (posts.length > 0) {
-// console.log.apply(null, log(`Loaded ${posts.length} posts in ${this.timerDelta().toFixed(2)}ms`));;
-// return posts;
-// }
-
-// // posts = await createTestData2(userID);
-
-// // log("Adding test data...");
-// // addDataArray(userID, posts);
-// // return await getData(userID, new Date(2022, 8), new Date());
-// }
-
-// async listUsers() {
-// let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
-// if (knownUsers.length === 0) {
-// return;
-// }
-
-// let preferredId = app.getPreferentialUserID()
-// for (let userID of knownUsers as string[]) {
-// // if (userID === preferredId) {
-// // continue;
-// // }
-
-// // let ids = await getAllIds(userID);
-// // if (ids.length === 0) {
-// // console.log.apply(null, log(`Purging user ${userID}`);
-// // indexedDB.deleteDatabase(`user_${userID}`);
-// // continue;
-// // }
-
-// console.log.apply(null, log(`${document.location.origin}/user/${userID}`));
-
-// // console.log.apply(null, log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID);
-// }
-
-// }
-
-
-// async initDB() {
-// let db = await openDatabase(this.userID);
-// }
-
-// query_findPeersForUser(message: any) {
-// let havePostsForUser = true;
-// if (havePostsForUser) {
-// return this.peerID;
-// }
-
-// return false;
-// }
-
- async registerRPCs() {
- if (!this.peerManager) {
- throw new Error();
- }
-
- this.peerManager.registerRPC('ping', (args: any) => {
- return {id:this.peerID, user:this.userID, user_name:this.username, peer_name:this.peername};
- });
-
- if (!this.isBootstrapPeer) {
- let pong = await this.peerManager.rpc.ping(this.peerManager.bootstrapPeerID);
- console.log.apply(null, log('pong from: ', pong));
- }
-
- // this.peerManager.registerRPC('getPostIDsForUser', (args: any) => {
- // this.sync.getPostsForUser
- // });
-
- }
-
-// async testPeerManager() {
-// if (!this.peerManager) {
-// throw new Error();
-// }
-
-// this.peerManager.registerRPC('getPostIDsForUser', (args: any) => {
-// return [1, 2, 3, 4, 5]
-// });
-
-// let postIDs = await this.peerManager.rpc.getPostIDsForUser("dummy_peer", "bloop");
-
-// console.log.apply(null, log("peerManager.rpc.getPostIDsForUser", postIDs));
-
-// // this.peerManager.registerSearchQuery('find_peers_for_user', this.query_findPeersForUser);
-
-// // let peers = await this.peerManager.search('find_peers_for_user', { 'user_id': 'bloop' });
-
-// }
-
- async main() {
-
- // Do capability detection here and report in a simple way if things we need don't exist with guidance on how to resolve it.
-
- // let urlParams = (new URL(globalThis.location.href)).searchParams;
- // if (urlParams.has('log')) {
- // this.showInfo();
- // }
-
- this.isHeadless = true;///\bHeadlessChrome\//.test(navigator.userAgent);
- this.isBootstrapPeer = true;//urlParams.has("bootstrap");
- if (this.isBootstrapPeer) {
- console.log.apply(null, log(`This is a bootstrap peer`));;
- }
-
- this.peerID = this.getPeerID();
- this.peername = this.getPeername();
- this.userID = this.getUserID();
- this.username = this.getUsername();
-
- this.connect();
-}
-
- // this.registerRPCs();
- // this.testPeerManager();
-
-// // let peer: RTCPeerConnection | null = null;
-// // // if (globalThis.RTCPeerConnection) {
-// // peer = new RTCPeerConnection({
-// // iceServers: [
-// // { urls: "stun:ddln.app" },
-// // // { urls: "turn:ddln.app", username: "a", credential: "b" },
-// // { urls: "stun:stun.l.google.com" }, // keeping this for now as my STUN server is not return ipv6
-// // // { urls: "stun:stun1.l.google.com" },
-// // // { urls: "stun:stun2.l.google.com" },
-// // // { urls: "stun:stun3.l.google.com" },
-// // // { urls: "stun:stun4.l.google.com" },
-// // ]
-// // });
-
-// // peer.createDataChannel('boop');
-
-// // peer.onicecandidate = ({ candidate }) => { log(`WRTC:${candidate?.address} ${candidate?.protocol} ${candidate?.type} ${(candidate as any)?.url}`) };
-// // peer.onnegotiationneeded = async (event) => {
-// // console.log.apply(null, log("on negotiation needed fired"));;
-
-// // let makingOffer = false;
-
-// // try {
-// // makingOffer = true;
-// // await peer.setLocalDescription();
-
-// // let IDsToSync = this.following;
-// // if (this.router.route === App.Route.USER) {
-// // IDsToSync = new Set([this.router.userID]);
-// // }
-
-// // if (!peer.localDescription) {
-// // return;
-// // }
-
-// // // this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync, peer.localDescription);
-// // // log(peer.localDescription.type + ":" + peer.localDescription.sdp);
-// // // this.initOffline(this.websocket);
-
-// // // this.websocket?.sendWebRTCDescription(peer.localDescription);
-// // } catch (err) {
-// // console.error(err);
-// // } finally {
-// // makingOffer = false;
-// // }
-
-// // }
-
-
-
-
-// // peer.createOffer().then((description)=>{
-// // peer.setLocalDescription(description)
-// // console.log.apply(null, log("RTC: " + description.sdp + description.type));;
-// // });
-
-// // }
-
-// // await this.exportPostsForUser('b38b623c-c3fa-4351-9cab-50233c99fa4e');
-
-// // Get initial state and route from URL and user agent etc
-
-// // Set local state (userid etc) based on that.
-
-// // Init libraries
-
-// // Render
-// // Load all images async
-// // Start the process of figuring out what posts we need
-// // Download posts once all current images are loaded
-
-
-// // globalThis.resizeTo(645, 900);
-
-// // this.initLogo()
-
-
-// this.getRoute();
-
-// if (this.router.route === App.Route.CONNECT) {
-// console.log.apply(null, log('connect', this.router.userID));
-// localStorage.setItem("dandelion_id", this.router.userID);
-// localStorage.removeItem("dandelion_username");
-// }
-
-
-// await this.initDB();
-
-// this.connectURL = `${document.location.origin}/connect/${this.userID}`;
-// document.getElementById('connectURL')!.innerHTML = `connect`;
-
-
-
-// this.isHeadless = urlParams.has('headless');
-
-// let limitPostsParam = urlParams.get('limitPosts');
-// if (limitPostsParam) {
-// this.limitPosts = parseInt(limitPostsParam);
-// }
-
-// let time = 0;
-// let delta = 0;
-// // let isPersisted = await navigator?.storage?.persisted();
-// // if (!isPersisted) {
-// // debugger;
-// // const isPersisted = await navigator.storage.persist();
-// // console.log.apply(null, log(`Persisted storage granted: ${isPersisted}`));;
-// // }
-
-// // log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
-
-// this.initMarkdown();
-
-// // let main = await fetch("/main.js");
-// // let code = await main.text();
-// // console.log.apply(null, log(code);
-// // registration.active.postMessage({type:"updateMain", code:code});
-
-// // this.posts = await this.loadPosts(userID) ?? [];
-
-// // debugger;
-
-// await this.render(); // , (postID:string)=>{this.deletePost(userID, postID)}
-
-// if ((performance as any)?.memory) {
-// console.log.apply(null, log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`));
-// }
-
-// // if (navigator?.storage) {
-// // let storageUsed = (await navigator?.storage?.estimate())?.usage/1024/1024
-// // }
-
-// // if (urlParams.get("sw") === "true") {
-// let registration;
-// registration = await this.registerServiceWorker();
-// // }
-
-// document.getElementById('username')!.innerText = `${this.username}`;
-// document.getElementById('peername')!.innerText = `peername:${this.peername}`;
-// document.getElementById('user_id')!.innerText = `user_id:${this.userID}`;
-// document.getElementById('peer_id')!.innerText = `peer_id:${this.peerID}`;
-
-// this.initButtons(this.userID, this.posts, registration);
-
-
-
-// console.log.apply(null, log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`));;
-
-// // await this.purgeEmptyUsers();
-
-
-
-// // this.listUsers()
-
-
-// // this.createNetworkViz();
-
-// // const client = new WebTorrent()
-
-// // // Sintel, a free, Creative Commons movie
-// // const torrentId = 'magnet:?xt=urn:btih:6091e199a8d9272a40dd9a25a621a5c355d6b0be&dn=WING+IT!+-+Blender+Open+Movie+1080p.mp4&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337';
-
-// // client.add(torrentId, function (torrent: any) {
-// // // Torrents can contain many files. Let's use the .mp4 file
-// // const file = torrent.files.find(function (file: any) {
-// // return file.name.endsWith('.mp4')
-// // })
-
-// // // Display the file by adding it to the DOM.
-// // // Supports video, audio, image files, and more!
-// // file.appendTo(document.getElementById('torrent-content'));
-// // })
-// }
-
-// renderWelcome(contentDiv: HTMLDivElement) {
-// contentDiv.innerHTML = `
-// Welcome to Dandelion v0.1!
-// Loading posts for the default feed...
-//
-// `;
-// }
-
-// // keep a map of posts to dom nodes.
-// // on re-render
-// // posts that are not in our list that we need at add
-// // posts that are in our list that we need to remove
-
-// private renderedPosts = new Map();
-
-// async render() {
-// if (this.isHeadless) {
-// console.log.apply(null, log('Headless so skipping render...'));
-// return;
-// }
-
-// performance.mark("render-start");
-// this.timerStart();
-
-
-// let existingPosts = this.posts;
-
-
-
-// this.posts = [];
-// switch (this.router.route) {
-// case App.Route.HOME:
-// case App.Route.CONNECT: {
-// this.following = new Set(await this.loadFollowersFromStorage(this.userID) ?? []);
-// this.posts = await this.getPostsForFeed();
-// // this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
-// // let compose = document.getElementById('compose');
-// // if (!compose) {
-// // break;
-// // }
-// // compose.style.display = "block";
-// break;
-// }
-// case App.Route.USER: {
-// this.posts = await this.loadPostsFromStorage(this.router.userID) ?? [];
-// let compose = document.getElementById('compose');
-// if (!compose) {
-// break;
-// }
-
-// compose.style.display = "none";
-// break;
-// }
-// case App.Route.POST: {
-// this.posts = await this.loadPostsFromStorage(this.router.userID, this.router.postID) ?? [];
-// let compose = document.getElementById('compose');
-// if (!compose) {
-// break;
-// }
-// compose.style.display = "none";
-// break;
-// }
-// default: {
-// console.log.apply(null, log("Render: got a route I didn't understand. Rendering HOME:", this.router.route));
-// this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
-// break;
-// }
-// }
-// let contentDiv = document.getElementById("content");
-// if (!contentDiv) {
-// throw new Error();
-// }
-// if (this.posts.length === 0) {
-// this.renderWelcome(contentDiv as HTMLDivElement);
-// return;
-// }
-
-
-// // let existingPostSet = new Set(existingPosts.map(post => post.post_id));
-// // let incomingPostSet = new Set(this.posts.map(post => post.post_id));
-
-// // let addedPosts = [];
-// // for (let post of this.posts) {
-// // if (!existingPostSet.has(post.post_id)) {
-// // addedPosts.push(post);
-// // }
-// // }
-
-// // let deletedPosts = [];
-// // for (let post of existingPosts) {
-// // if (!incomingPostSet.has(post.post_id)) {
-// // deletedPosts.push(post);
-// // }
-// // }
-
-// // console.log.apply(null, log("added:", addedPosts, "removed:", deletedPosts);
-
-// const fragment = document.createDocumentFragment();
-
-// contentDiv.innerHTML = "";
-// let count = 0;
-
-// this.renderedPosts.clear();
-// let first = true;
-// for (let i = this.posts.length - 1; i >= 0; i--) {
-// let postData = this.posts[i];
-// // this.postsSet.add(postData);
-// // TODO return promises for all image loads and await those.
-// let post = this.renderPost(postData.data, first);
-// first = false;
-// // this.renderedPosts.set(postData.post_id, post);
-// if (post) {
-// fragment.appendChild(post);
-// count++;
-// }
-// if (count > this.limitPosts) {
-// break;
-// }
-// }
-
-
-// if (!contentDiv) {
-// throw new Error("Couldn't get content div!");
-// }
-
-// contentDiv.appendChild(fragment);
-
-// let renderTime = this.timerDelta();
-
-// console.log.apply(null, log(`render took: ${renderTime.toFixed(2)}ms`));;
-// performance.mark("render-end");
-// performance.measure('render-time', 'render-start', 'render-end');
-
-
-
-// // if ((performance as any)?.memory) {
-// // console.log.apply(null, log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`));
-// // }
-
-// }
-
-// async deletePost(userID: string, postID: string) {
-// deleteData(userID, postID)
-
-// this.render();
-// }
-
-// renderPost(post: Post, first: boolean) {
-// if (!(post.hasOwnProperty("text"))) {
-// throw new Error("Post is malformed!");
-// }
-// let containerDiv = document.createElement("div");
-
-// let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`;
-
-// let deleteButton = document.createElement('button'); deleteButton.innerText = 'delete';
-// deleteButton.onclick = () => { this.deletePost(post.author_id, post.post_id) };
-
-// // let editButton = document.createElement('button'); editButton.innerText = 'edit';
-// let shareButton = document.createElement('button'); shareButton.innerText = 'share';
-// shareButton.onclick = async () => {
-// 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;
-// if (this.markedAvailable) {
-// markdown = marked.parse(post.text);
-// }
-
-// // if (markdown.includes("