// 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 } 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 {
constructor() {
this.username = '';
this.peername = '';
this.userID = '';
this.peerID = '';
this.following = new Set();
// posts: StoragePost[] = [];
this.isHeadless = false;
this.isBootstrapPeer = false;
this.showLog = false;
this.markedAvailable = false;
this.limitPosts = 50;
// websocket: wsConnection | null = null;
// vizGraph: any | null = null;
this.qrcode = null;
this.connectURL = "";
this.firstRun = false;
this.peerManager = null;
this.animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal'];
this.adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy'];
this.snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait'];
// 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("