From a96d7e28c88da2ee1e99fc5220ff65d9fe82705f Mon Sep 17 00:00:00 2001 From: bobbydigitales Date: Wed, 4 Jun 2025 02:55:24 -0700 Subject: [PATCH] remove bootstrap main experiment for now --- src/App.ts | 1 + src/bootstrap_main.ts | 2157 ----------------------------------------- static/App.js | 1 + 3 files changed, 2 insertions(+), 2157 deletions(-) delete mode 100644 src/bootstrap_main.ts 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("${first ? '' : '
'} -//
-// @${post.author} - -// ${post.post_timestamp.toLocaleDateString()} -// -// ${ownPost ? `` : ''} -// ${ownPost ? `` : ''} -// -//
-//
${markdown}
-// ` - -// containerDiv.innerHTML = postTemplate; - - -// if (ownPost) { -// containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton); -// // containerDiv.querySelector('#editButton')?.appendChild(editButton); -// } - - -// containerDiv.querySelector('#shareButton')?.appendChild(shareButton); - - -// if (!("image_data" in post && post.image_data)) { -// // containerDiv.appendChild(timestampDiv); -// return containerDiv; -// // return null; -// } - -// let image = document.createElement("img"); -// image.title = `${(post.image_data.byteLength / 1024 / 1024).toFixed(2)}MBytes`; -// // const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' }); -// const blob = new Blob([post.image_data as ArrayBuffer]); -// const url = URL.createObjectURL(blob); -// image.onload = () => { -// URL.revokeObjectURL(url); -// }; - -// image.src = url; -// // image.src = image.src = "data:image/png;base64," + post.image; -// image.className = "postImage"; -// // image.onclick = () => { App.maximizeElement(image) }; - -// containerDiv.appendChild(image); -// // containerDiv.appendChild(timestampDiv); - -// return containerDiv; -// } - -// static maximizeElement(element: HTMLImageElement) { -// element.style.transform = "scale(2.0)" -// } - -// router = { -// route: App.Route.HOME, -// userID: '', -// postID: '', -// mediaID: '' -// } - -// getRoute() { -// let path = document.location.pathname; -// console.log.apply(null, log("router: path ", path)); - -// const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))"; - -// const match = path.match(new RegExp(regex)); - -// if (match) { -// if (match[8]) { // Check for the connect route -// this.router.userID = match[8]; -// this.router.route = App.Route.CONNECT; -// } else { - -// this.router.userID = match[2]; -// this.router.postID = match[4]; -// this.router.mediaID = match[6]; - -// if (this.router.mediaID) { -// this.router.route = App.Route.MEDIA; -// } else if (this.router.postID) { -// this.router.route = App.Route.POST; -// } else { -// this.router.route = App.Route.USER; -// } -// } -// } - -// console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route])); - -// // user = /user/ -// // post = /user//post/ -// // media = /user//post//media/ -// // group = /group/ID/post/ -// // hashtag = /hashtag/ -- maybe only hastags in groups -// // home = / - -// } - -// } - -// namespace App { -// export enum Route { -// USER, -// POST, -// MEDIA, -// GROUP, -// HOME, -// CONNECT, -// }; - -// // export function connect() { -// // throw new Error("Function not implemented."); -// // } - -// // export function connect() { -// // throw new Error("Function not implemented."); -// // } -} - - - -let app = new App(); - -app.main(); - -// globalThis.addEventListener("load", app.main.bind(app)); diff --git a/static/App.js b/static/App.js index 64daf94..740f769 100644 --- a/static/App.js +++ b/static/App.js @@ -177,6 +177,7 @@ export class App { continue; } 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); this.statusBar.updatePeerStatus(sendingPeerID, `syncing(${logID(userID)} ${postIDs.length})`); console.log.apply(null, log(`[app] Got (${postIDs.length}) post IDs for user [${logID(userID)}] from peer [${logID(sendingPeerID)}]`));