diff --git a/src/PeerManager.ts b/src/PeerManager.ts index d1cb777..d92db1f 100644 --- a/src/PeerManager.ts +++ b/src/PeerManager.ts @@ -43,6 +43,7 @@ export class PeerManager { messageSuperlog = false; watchdogInterval: number = 0; reconnectTimer: number | null = null; + peerStateSuperlog: boolean = false; // async watchdog() { // // Check that we're connected to at least N peers. If not, reconnect to the bootstrap server. @@ -151,7 +152,7 @@ export class PeerManager { } if (!bootstrapPeerID) { - // console.log.apply(null, log("Didn't get bootstrap peer, waiting 10 seconds...")); + console.log.apply(null, log("Didn't get bootstrap peer, waiting 10 seconds...")); // let callSendHello2OnTimeout = () => { console.log(this, "jajajajaj");this.sendHello2() }; // setTimeout(callSendHello2OnTimeout, 5_000); return; @@ -323,7 +324,7 @@ export class PeerManager { } onPeerConnected(peerID: PeerID) { - console.log.apply(null, log(`PeerManager: Successfully connected to peer ${peerID}`)); + this.peerStateSuperlog && console.log.apply(null, log(`PeerManager: Successfully connected to peer ${peerID}`)); this.dispatchEvent(PeerEventTypes.PEER_CONNECTED, { peerID: peerID }); } @@ -470,7 +471,7 @@ class PeerConnection { 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:stun.l.google.com" }, // keeping this for now as my STUN server is not returning ipv6 // { urls: "stun:stun1.l.google.com" }, // { urls: "stun:stun2.l.google.com" }, // { urls: "stun:stun3.l.google.com" }, diff --git a/src/Sync.ts b/src/Sync.ts index 22284ff..08cfe40 100644 --- a/src/Sync.ts +++ b/src/Sync.ts @@ -39,8 +39,8 @@ export class Sync { userIDsToSync: Set = new Set(); syncSuperlog: boolean = false; - setArchive(isHeadless: boolean) { - this.isArchivePeer = isHeadless; + setArchive(isArchive: boolean) { + this.isArchivePeer = isArchive; } setUserID(userID: string) { @@ -130,6 +130,14 @@ export class Sync { let following = ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :) following.push(this.userID); + // Hazel + if (userID == '622ecc28-2eff-44b9-b89d-fdea7c8dd2d5') { + following.push(...[ + '8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona + '622ecc28-2eff-44b9-b89d-fdea7c8dd2d5', // Hazel + ]); + } + // Rob if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') { following.push(...[ @@ -137,6 +145,7 @@ export class Sync { '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin 'bba3ad24-9181-4e22-90c8-c265c80873ea', // Harry '8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona + '622ecc28-2eff-44b9-b89d-fdea7c8dd2d5', // Hazel ]); } diff --git a/src/db.ts b/src/db.ts index 5cebf22..192af50 100644 --- a/src/db.ts +++ b/src/db.ts @@ -281,7 +281,7 @@ export async function checkPostIds(userID: string, post_ids: string[]) { } } - console.log(`checkPostIds need ${postIdsNeeded.length} posts`); + // console.log(`checkPostIds need ${postIdsNeeded.length} posts`); return postIdsNeeded; } catch (error) { console.error("Error in opening database:", error); diff --git a/src/log.ts b/src/log.ts index c268602..5ade2c5 100644 --- a/src/log.ts +++ b/src/log.ts @@ -32,6 +32,9 @@ export function log(...args: any[]): any { let logLine = `[${new Date().toLocaleTimeString()}]: `; for (let arg of args) { let completeLine = (typeof arg === "string" || arg instanceof String) ? arg : JSON.stringify(arg, null, 4); + if (completeLine === undefined) { + completeLine = "undefined"; + } logLine += completeLine.substring(0, 500); } logLines.push(logLine + "\n"); @@ -42,5 +45,5 @@ export function log(...args: any[]): any { renderLog(); - return [...args]; + return [logLine];// [...args]; } diff --git a/src/main2.ts b/src/main2.ts index 1d084d5..9a155ba 100644 --- a/src/main2.ts +++ b/src/main2.ts @@ -1,7 +1,4 @@ // TODO: virtual list, only rerender what's needed so things can keep playing. - - - /* Problems 1. Can't delete, very annoying @@ -29,134 +26,8 @@ Restruucture the app around the data. App/WS split is messy. Clean it up. */ +import { App } from "App"; -// 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"; -import { PeerManager, PeerEventTypes } from "PeerManager"; -import { Sync } from "Sync"; - -import { log, logID, renderLog, setLogVisibility } from "log" - -// 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; - } -} window.addEventListener('scroll', () => { // Total height of the document @@ -173,1444 +44,6 @@ window.addEventListener('scroll', () => { }); - - -// 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); -} - - - -class App { - username: string = ''; - peername: string = ''; - userID: string = ''; - peerID: string = ''; - following: Set = new Set(); - posts: StoragePost[] = []; - isHeadless: boolean = false; - isBootstrapPeer: boolean = false; - isArchivePeer: 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; - sync: Sync = new Sync(); - renderTimer: number = 0; - - async announceUser_rpc_response(sendingPeerID: string, userIDs: string[]) { - if (this.isBootstrapPeer) { - return; - } - - console.log.apply(null, log(`announceUsers from ${sendingPeerID}`, userIDs)); - - for (let userID of userIDs) { - // console.log.apply(null, log(`[app] announceUsers, got user:${userID} from peer ${sendingPeerID}`)); - this.sync.addUserPeer(userID, sendingPeerID); - if (this.sync.shouldSyncUserID(userID) || (this.router.route === App.Route.USER && userID === this.router.userID)) { - let postIDs = await this.peerManager?.rpc.getPostIDsForUser(sendingPeerID, userID); - // console.log.apply(null, log(`[app] announceUsers response, gotPostIDs`, postIDs)); - let neededPostIDs = await this.sync.checkPostIds(userID, sendingPeerID, postIDs); - // console.log.apply(null, log(`[app] announceUsers needed posts`, neededPostIDs)); - - if (neededPostIDs.length > 0) { - let neededPosts = await this.peerManager?.rpc.getPostsForUser(sendingPeerID, this.peerID, userID, neededPostIDs); - console.log(neededPosts); - } - - } - }; - } - - - async connect() { - this.peerManager = new PeerManager(this.userID, this.peerID, this.isBootstrapPeer); - if (this.peerManager === null) { - throw new Error(); - } - // this.registerRPCs(); - - this.peerManager.addEventListener(PeerEventTypes.PEER_CONNECTED, async (event: any) => { - if (!this.peerManager) { - throw new Error(); - } - console.log.apply(null, log(`[app]: peer connected:${event.peerID}`)); - - if (this.isBootstrapPeer) { - return; - } - - let knownUsers = await this.sync.getKnownUsers(); - this.peerManager.rpc.announceUsers(event.peerID, this.peerID, knownUsers); - // rpc saying what peers we have - }); - - this.peerManager.addEventListener(PeerEventTypes.PEER_DISCONNECTED, async (event: any) => { - console.log.apply(null, log(`[app]: peer disconnected:${event.peerID}`)); - }); - - - 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('announceUsers', (sendingPeerID: string, userIDs: string[]) => { - this.announceUser_rpc_response(sendingPeerID, userIDs); - }); - - this.peerManager.registerRPC('getPeersForUser', (userID: string) => { - return [1, 2, 3, 4, 5]; - }); - - - this.peerManager.registerRPC('getPostIDsForUser', async (userID: string) => { - let postIDs = await this.sync.getPostIdsForUser(userID); - if (postIDs) { - return postIDs; - } - }); - - this.peerManager.registerRPC('getPostsForUser', async (requestingPeerID:string, userID:string, postIDs: string[]) => { - let posts = await this.sync.getPostsForUser(userID, postIDs); - - - for (let post of posts) { - console.log.apply(null, log(`[app] sendPostForUser sending post [${logID(post.post_id)}] to [${logID(requestingPeerID)}]`, userID, post)); - - this.peerManager?.rpc.sendPostForUser(requestingPeerID, userID, post); - } - // return posts; - - // return postIDs; - }); - - this.peerManager.registerRPC('sendPostForUser', async (userID:string, post:Post) => { - console.log.apply(null, log(`[app] sendPostForUser got post`, userID, post)); - // if (post.text === "image...") { - // debugger; - // } - await this.sync.writePostForUser(userID, post); - // if (userID === this.userID) { - - if (this.renderTimer) { - clearTimeout(this.renderTimer); - } - - this.renderTimer = setTimeout(()=>{this.render()}, 200); - // } - }); - - - await this.peerManager.connect(); - console.log.apply(null, log("*************** after peerManager.connect"));; - - - if (this.isBootstrapPeer) { - return; - } - - // let usersToSync = await Sync.getFollowing(this.userID); - - // for (let userID of usersToSync) { - // console.log(userID); - // // this.peerManager.rpc.getPeersForUser(userID); - // } - - - // for (let userID in this.sync.usersToSync()) { - // let peers = await this.peerManager.rpc.getPeersForUser(userID); - - // for (let peer in peers) { - // let peer = await this.peerManager.connectToPeer(userID); - - // let postIDs = peer.getPostIDsForUser(userID); - - // let postIDsNeeded = this.sync.checkPostIds(userID, postIDs); - - // if (postIDs.length === 0) { - // continue; - // } - - // let posts = peer.rpc.getPostsForUser(userID, postIDs); - - // this.sync.writePostsForUser(userID, posts); - - // this.render(); - // } - - // } - - // 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 = window.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 - window.URL.revokeObjectURL(url); - } - - downloadJson(data: any, filename = 'data.json') { - const jsonString = JSON.stringify(data); - const blob = new Blob([jsonString], { type: 'application/json' }); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.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("window.YTD.tweets.part0", "window.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 - // window.addEventListener('offline', () => { - // console.log.apply(null, log("offline")); - // }); - - // // Event listener for going online - // window.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 => window.location.href = `${window.location.origin}/`) - - let profileButton = this.div('profile-button'); - profileButton.addEventListener('click', e => window.location.href = `${window.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('window.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 { - return this.sync.getFollowing(userID); - } - - 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('getPeersForUser', (userID: any) => { - return [1, 2, 3, 4, 5]; - }); - - - // 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(window.location.href)).searchParams; - if (urlParams.has('log')) { - this.showInfo(); - } - - this.isHeadless = /\bHeadlessChrome\//.test(navigator.userAgent) || urlParams.has('headless'); - this.isArchivePeer = urlParams.has('archive'); - this.isBootstrapPeer = urlParams.has("bootstrap"); - - console.log(`[headless]${this.isHeadless} [archive] ${this.isArchivePeer} [bootstrap] ${this.isBootstrapPeer}`); - - let limitPostsParam = urlParams.get('limitPosts'); - if (limitPostsParam) { - this.limitPosts = parseInt(limitPostsParam); - } - - this.peerID = this.getPeerID(); - this.peername = this.getPeername(); - this.userID = this.getUserID(); - this.username = this.getUsername(); - - this.sync.setUserID(this.userID) - this.sync.setArchive(this.isArchivePeer); - - 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"); - } - - this.connect(); - - await this.initDB(); - - this.connectURL = `${document.location.origin}/connect/${this.userID}`; - document.getElementById('connectURL')!.innerHTML = `connect`; - - - - - 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(); window.addEventListener("load", app.main.bind(app)); diff --git a/src/sw.ts b/src/sw.ts index b0f74b0..6d858e3 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -11,6 +11,11 @@ const contentToCache = [ '/static/db.js', '/static/PeerManager.js', '/static/IDUtils.js', + '/static/dataUtils.js', + '/static/App.js', + '/static/Sync.js', + '/static/IDUtils.js', + '/static/log.js', '/static/favicon.ico' ]; diff --git a/static/db.js b/static/db.js index 92feacc..782a0f0 100644 --- a/static/db.js +++ b/static/db.js @@ -210,7 +210,7 @@ export async function checkPostIds(userID, post_ids) { console.error("Error processing post:", error); } } - console.log(`checkPostIds need ${postIdsNeeded.length} posts`); + // console.log(`checkPostIds need ${postIdsNeeded.length} posts`); return postIdsNeeded; } catch (error) { diff --git a/static/index.html b/static/index.html index e495029..c6bea6a 100644 --- a/static/index.html +++ b/static/index.html @@ -14,7 +14,9 @@ "IDUtils": "/static/IDUtils.js", "PeerManager": "/static/PeerManager.js", "log": "/static/log.js", - "Sync": "/static/Sync.js" + "Sync": "/static/Sync.js", + "App": "/static/App.js", + "dataUtils": "/static/dataUtils.js" } } diff --git a/static/main2.js b/static/main2.js index 23f8591..48bca43 100644 --- a/static/main2.js +++ b/static/main2.js @@ -25,24 +25,7 @@ user 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, deleteData, getAllData } from "db"; -import { generateID } from "IDUtils"; -import { PeerManager, PeerEventTypes } from "PeerManager"; -import { Sync } from "Sync"; -import { log, logID, renderLog, setLogVisibility } from "log"; -class Post { - constructor(author, author_id, text, post_timestamp, imageData = null, importedFrom = null, importSource = 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; - } -} +import { App } from "App"; window.addEventListener('scroll', () => { // Total height of the document const totalPageHeight = document.body.scrollHeight; @@ -54,1091 +37,5 @@ window.addEventListener('scroll', () => { // 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, 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) { - var bytes = new Uint8Array(buffer); - return (await bytesToBase64DataUrl(bytes)).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) { - // 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); -} -class App { - constructor() { - this.username = ''; - this.peername = ''; - this.userID = ''; - this.peerID = ''; - this.following = new Set(); - this.posts = []; - this.isHeadless = false; - this.isBootstrapPeer = false; - this.isArchivePeer = 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.sync = new Sync(); - this.renderTimer = 0; - this.time = 0; - 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']; - // 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 - this.renderedPosts = new Map(); - this.router = { - route: App.Route.HOME, - userID: '', - postID: '', - mediaID: '' - }; - } - async announceUser_rpc_response(sendingPeerID, userIDs) { - if (this.isBootstrapPeer) { - return; - } - console.log.apply(null, log(`announceUsers from ${sendingPeerID}`, userIDs)); - for (let userID of userIDs) { - // console.log.apply(null, log(`[app] announceUsers, got user:${userID} from peer ${sendingPeerID}`)); - this.sync.addUserPeer(userID, sendingPeerID); - if (this.sync.shouldSyncUserID(userID) || (this.router.route === App.Route.USER && userID === this.router.userID)) { - let postIDs = await this.peerManager?.rpc.getPostIDsForUser(sendingPeerID, userID); - // console.log.apply(null, log(`[app] announceUsers response, gotPostIDs`, postIDs)); - let neededPostIDs = await this.sync.checkPostIds(userID, sendingPeerID, postIDs); - // console.log.apply(null, log(`[app] announceUsers needed posts`, neededPostIDs)); - if (neededPostIDs.length > 0) { - let neededPosts = await this.peerManager?.rpc.getPostsForUser(sendingPeerID, this.peerID, userID, neededPostIDs); - console.log(neededPosts); - } - } - } - ; - } - async connect() { - this.peerManager = new PeerManager(this.userID, this.peerID, this.isBootstrapPeer); - if (this.peerManager === null) { - throw new Error(); - } - // this.registerRPCs(); - this.peerManager.addEventListener(PeerEventTypes.PEER_CONNECTED, async (event) => { - if (!this.peerManager) { - throw new Error(); - } - console.log.apply(null, log(`[app]: peer connected:${event.peerID}`)); - if (this.isBootstrapPeer) { - return; - } - let knownUsers = await this.sync.getKnownUsers(); - this.peerManager.rpc.announceUsers(event.peerID, this.peerID, knownUsers); - // rpc saying what peers we have - }); - this.peerManager.addEventListener(PeerEventTypes.PEER_DISCONNECTED, async (event) => { - console.log.apply(null, log(`[app]: peer disconnected:${event.peerID}`)); - }); - 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('announceUsers', (sendingPeerID, userIDs) => { - this.announceUser_rpc_response(sendingPeerID, userIDs); - }); - this.peerManager.registerRPC('getPeersForUser', (userID) => { - return [1, 2, 3, 4, 5]; - }); - this.peerManager.registerRPC('getPostIDsForUser', async (userID) => { - let postIDs = await this.sync.getPostIdsForUser(userID); - if (postIDs) { - return postIDs; - } - }); - this.peerManager.registerRPC('getPostsForUser', async (requestingPeerID, userID, postIDs) => { - let posts = await this.sync.getPostsForUser(userID, postIDs); - for (let post of posts) { - console.log.apply(null, log(`[app] sendPostForUser sending post [${logID(post.post_id)}] to [${logID(requestingPeerID)}]`, userID, post)); - this.peerManager?.rpc.sendPostForUser(requestingPeerID, userID, post); - } - // return posts; - // return postIDs; - }); - this.peerManager.registerRPC('sendPostForUser', async (userID, post) => { - console.log.apply(null, log(`[app] sendPostForUser got post`, userID, post)); - // if (post.text === "image...") { - // debugger; - // } - await this.sync.writePostForUser(userID, post); - // if (userID === this.userID) { - if (this.renderTimer) { - clearTimeout(this.renderTimer); - } - this.renderTimer = setTimeout(() => { this.render(); }, 200); - // } - }); - await this.peerManager.connect(); - console.log.apply(null, log("*************** after peerManager.connect")); - ; - if (this.isBootstrapPeer) { - return; - } - // let usersToSync = await Sync.getFollowing(this.userID); - // for (let userID of usersToSync) { - // console.log(userID); - // // this.peerManager.rpc.getPeersForUser(userID); - // } - // for (let userID in this.sync.usersToSync()) { - // let peers = await this.peerManager.rpc.getPeersForUser(userID); - // for (let peer in peers) { - // let peer = await this.peerManager.connectToPeer(userID); - // let postIDs = peer.getPostIDsForUser(userID); - // let postIDsNeeded = this.sync.checkPostIds(userID, postIDs); - // if (postIDs.length === 0) { - // continue; - // } - // let posts = peer.rpc.getPostsForUser(userID, postIDs); - // this.sync.writePostsForUser(userID, posts); - // this.render(); - // } - // } - // 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, title, text) => { - 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; - } - timerStart() { - this.time = performance.now(); - } - timerDelta() { - return performance.now() - this.time; - } - getFixedTweetText(entry) { - 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, filename, mimeType = '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 = window.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 - window.URL.revokeObjectURL(url); - } - downloadJson(data, filename = 'data.json') { - const jsonString = JSON.stringify(data); - const blob = new Blob([jsonString], { type: 'application/json' }); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } - async importPostsForUser(userID, posts) { - } - async exportPostsForUser(userID) { - let posts = await getAllData(userID); - let output = []; - console.log.apply(null, log("Serializing images")); - for (let post of posts) { - let newPost = post.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, tweetArchive) { - console.log.apply(null, log("Importing tweet archive")); - let postsTestData = []; - // let response = await fetch("./tweets.js"); - // let tweetsText = await response.text(); - // tweetsText = tweetsText.replace("window.YTD.tweets.part0", "window.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 = 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) { - 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, mimeType, quality = 0.5) { - 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) : 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.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, postText, mediaData, mimeType) { - 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.byteLength > 500 * 1024) { - let compressedImage = await this.compressImage(mediaData, mimeType, 0.9); - if (compressedImage) { - mediaData = compressedImage; - } - } - 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; - } - hashIdToIndices(id) { - 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, listOne, listTwo) { - 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, fontSize) { - 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 - // window.addEventListener('offline', () => { - // console.log.apply(null, log("offline")); - // }); - // // Event listener for going online - // window.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) { - 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) { - // Always return a Promise - return new Promise((resolve, reject) => { - let content = ''; - const reader = new FileReader(); - // Wait till complete - reader.onloadend = function (e) { - content = e.target.result; - resolve(content); - }; - // Make sure to handle error states - reader.onerror = function (e) { - 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').classList.add('qrcode_image'); - document.querySelector('#qrcode > canvas').classList.add('qrcode_image'); - this.showLog = true; - } - button(elementName) { - return document.getElementById(elementName); - } - div(elementName) { - return document.getElementById(elementName); - } - initButtons(userID, posts, registration) { - // 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 => window.location.href = `${window.location.origin}/`); - let profileButton = this.div('profile-button'); - profileButton.addEventListener('click', e => window.location.href = `${window.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'); - filePicker?.addEventListener('change', async (event) => { - for (let file of filePicker.files) { - 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) => { - 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('window.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"); - let postText = document.getElementById("textarea_post"); - 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); - }); - 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 = []; - 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) { - return this.sync.getFollowing(userID); - } - async loadPostsFromStorage(userID, postID) { - this.timerStart(); - let posts = []; - // 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) { - // 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) { - let havePostsForUser = true; - if (havePostsForUser) { - return this.peerID; - } - return false; - } - async registerRPCs() { - if (!this.peerManager) { - throw new Error(); - } - this.peerManager.registerRPC('ping', (args) => { - 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('getPeersForUser', (userID) => { - return [1, 2, 3, 4, 5]; - }); - // 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(window.location.href)).searchParams; - if (urlParams.has('log')) { - this.showInfo(); - } - this.isHeadless = /\bHeadlessChrome\//.test(navigator.userAgent) || urlParams.has('headless'); - this.isArchivePeer = urlParams.has('archive'); - this.isBootstrapPeer = urlParams.has("bootstrap"); - console.log(`[headless]${this.isHeadless} [archive] ${this.isArchivePeer} [bootstrap] ${this.isBootstrapPeer}`); - let limitPostsParam = urlParams.get('limitPosts'); - if (limitPostsParam) { - this.limitPosts = parseInt(limitPostsParam); - } - this.peerID = this.getPeerID(); - this.peername = this.getPeername(); - this.userID = this.getUserID(); - this.username = this.getUsername(); - this.sync.setUserID(this.userID); - this.sync.setArchive(this.isArchivePeer); - 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"); - } - this.connect(); - await this.initDB(); - this.connectURL = `${document.location.origin}/connect/${this.userID}`; - document.getElementById('connectURL').innerHTML = `connect`; - 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?.memory) { - console.log.apply(null, log(`memory used: ${(performance.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) { - contentDiv.innerHTML = `
- Welcome to Dandelion v0.1!
- Loading posts for the default feed... -
- `; - } - 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); - 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, postID) { - deleteData(userID, postID); - this.render(); - } - renderPost(post, first) { - 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]); - 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) { - element.style.transform = "scale(2.0)"; - } - 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 = / - } -} -(function (App) { - let Route; - (function (Route) { - Route[Route["USER"] = 0] = "USER"; - Route[Route["POST"] = 1] = "POST"; - Route[Route["MEDIA"] = 2] = "MEDIA"; - Route[Route["GROUP"] = 3] = "GROUP"; - Route[Route["HOME"] = 4] = "HOME"; - Route[Route["CONNECT"] = 5] = "CONNECT"; - })(Route = App.Route || (App.Route = {})); - ; - // export function connect() { - // throw new Error("Function not implemented."); - // } - // export function connect() { - // throw new Error("Function not implemented."); - // } -})(App || (App = {})); let app = new App(); window.addEventListener("load", app.main.bind(app)); diff --git a/static/sw.js b/static/sw.js index f10da19..3efc01e 100644 --- a/static/sw.js +++ b/static/sw.js @@ -11,6 +11,11 @@ const contentToCache = [ '/static/db.js', '/static/PeerManager.js', '/static/IDUtils.js', + '/static/dataUtils.js', + '/static/App.js', + '/static/Sync.js', + '/static/IDUtils.js', + '/static/log.js', '/static/favicon.ico' ]; self.addEventListener("install", (e) => {