// TODO: virtual list, only rerender what's needed so things can keep playing. import { getData, addData, addDataArray, clearData, deleteData, mergeDataArray, checkPostIds, getAllIds, getPostsByIds } from "./db.js"; // 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) { return new Promise(resolve => setTimeout(resolve, durationMs)); } function uuidv4() { return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); } function uuidToBytes(uuid) { 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) { 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) { const bytes = uuidToBytes(uuid); return encodeBase58(bytes); } function logID(ID) { return ID.substring(0, 5); } let logLines = []; let logLength = 10; function log(message) { console.log(message); logLines.push(`${new Date().toLocaleTimeString()}: ${message}`); if (logLines.length > 10) { logLines = logLines.slice(logLines.length - logLength); } let log = document.getElementById("log"); if (!log) { throw new Error(); } log.innerText = logLines.join("\n"); } function generateID() { if (self.crypto.hasOwnProperty("randomUUID")) { return self.crypto.randomUUID(); } return uuidv4(); } 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; } } window.addEventListener('scroll', () => { // Total height of the document const totalPageHeight = document.body.scrollHeight; // Current scroll position const scrollPoint = window.scrollY + window.innerHeight; // Check if scrolled to bottom if (scrollPoint >= totalPageHeight) { console.log('Scrolled to the bottom!'); console.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,", ""); } // function base64ToArrayBuffer(base64: string) { // var binaryString = atob(base64); // var bytes = new Uint8Array(binaryString.length); // for (var i = 0; i < binaryString.length; i++) { // bytes[i] = binaryString.charCodeAt(i); // } // return bytes.buffer; // } async function base64ToArrayBuffer(base64String) { let response = await fetch("data:application/octet-stream;base64," + base64String); let arrayBuffer = await response.arrayBuffer(); return arrayBuffer; // let buffer = new Uint8Array(arrayBuffer); // return buffer; } 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 wsConnection { async send(message) { let json = ""; try { json = JSON.stringify(message); // console.log("*******", (await compressString(json)).byteLength, json.length); } catch (e) { console.log(e, "wsConnection send: Couldn't serialize message", message); } // log(`ws->${json.slice(0, 240)}`) this.websocket.send(json); } helloResponseHandler(data) { debugger; let users = []; try { let currentUserPeers = data.userPeers[app.router.userID]; users.push([app.router.userID, data.userPeers[app.router.userID]]); delete data.userPeers[app.router.userID]; } catch (e) { console.log('helloResponseHandler', e); } users = [...users, ...Object.entries(data.userPeers)]; log(`Net: got ${users.length} users from bootstrap peer. ${users.join(',')}`); for (let [userID, peerIDs] of users) { this.peers.set(userID, [...peerIDs]); for (let peerID of [...peerIDs]) { if (peerID === this.peerID) { continue; } 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 } }); } } } pongHandler(data) { } async getPostIdsForUserResponseHandler(data) { // log(`getPostsForUserResponse: ${data}`) let message = data.message; log(`Net: got ${message.post_ids.length} post IDs for user ${logID(data.message.user_id)} from peer ${logID(data.from)}`); // console.log(`Checking post IDs...`); let postIds = await checkPostIds(message.user_id, data.message.post_ids); if (postIds.length === 0) { log(`Don't need any posts for user ${logID(data.message.user_id)} from peer ${logID(data.from)}`); return; } 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; // } async getPostIdsForUserHandler(data) { let message = data.message; let postIds = await getAllIds(message.user_id) ?? []; if (postIds.length === 0) { 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; } 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); } // Send posts to peer async getPostsForUserHandler(data) { let message = data.message; let posts = await getPostsByIds(message.user_id, message.post_ids) ?? []; log(`Net: Sending ${posts.length} posts for user ${logID(message.user_id)} to peer ${logID(data.from)}`); app.timerStart(); let output = []; for (let post of posts) { let newPost = post.data; if (newPost.image_data) { // let compressedData = await wsConnection.compressArrayBuffer(newPost.image_data); // console.log((newPost.image_data.byteLength - compressedData.byteLength) / 1024 / 1024); newPost.image_data = await arrayBufferToBase64(newPost.image_data); } // let megs = JSON.stringify(newPost).length/1024/1024; // console.log(`getPostsForUserHandler id:${newPost.post_id} post length:${megs}`); output.push(newPost); } // posts = posts.map((post:any)=>{let newPost = post.data; if (newPost.image_data){newPost.image_data = arraybufferto};return newPost}); // posts = posts.map((post:any)=>{}) 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 } }; this.send(responseMessage); let sendTime = app.timerDelta(); log(`send took: ${sendTime.toFixed(2)}ms`); } // Got posts from peer async getPostsForUserReponseHandler(data) { app.timerStart(); let message = data.message; console.log(`Net: got ${message.posts.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`); for (let post of message.posts) { 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(`Merging same user peer posts...`); await mergeDataArray(message.user_id, data.message.posts); let receiveTime = app.timerDelta(); log(`Receive took: ${receiveTime.toFixed(2)}ms`); if (message.user_id === app.router.userID) { app.render(); } } async peerMessageHandler(data) { // log(`peerMessageHandler ${JSON.stringify(data)}`) 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); } connect() { if (this.websocket?.readyState === WebSocket.OPEN) { return; } window.clearInterval(this.websocketPingInterval); if (this.websocket) { this.websocket.close(); } ; try { this.websocket = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/ws`); } catch (error) { console.log(error.message); return; } this.websocket.onopen = async (event) => { log("ws:connected"); let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', '')); console.log('Net: Sending known users', knownUsers); this.send({ type: "hello", user_id: this.userID, user_name: app.username, peer_id: this.peerID, peer_name: app.peername, known_users: knownUsers }); this.websocketPingInterval = window.setInterval(() => { if (!navigator.onLine) { return; } this.send({ type: "ping", peer_id: this.peerID }); }, 10000); }; this.websocket.onclose = (event) => { log("ws:disconnected"); // this.retry *= 2; log(`Retrying in ${this.retry} seconds`); window.setTimeout(() => { this.connect(); }, this.retry * 1000); }; this.websocket.onmessage = (event) => { // log('ws:<-' + event.data.slice(0, 240)); debugger; 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) => { log('ws:error: ' + event); }; } disconnect() { this.websocket?.close(); } constructor(userID, peerID) { this.websocket = null; this.userID = ""; this.peerID = ""; this.websocketPingInterval = 0; this.retry = 10; this.state = 'disconnected'; this.peers = new Map(); this.messageHandlers = new Map(); this.peerMessageHandlers = new Map(); this.userID = userID; this.peerID = peerID; this.messageHandlers.set('hello', this.helloResponseHandler.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.connect(); if (!this.websocket) { // set a timer and retry? } } } class App { constructor() { this.username = ''; this.peername = ''; this.userID = ''; this.peerID = ''; this.following = []; this.posts = []; this.isHeadless = false; this.showLog = false; 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: '' }; } initMarkdown() { const renderer = new marked.Renderer(); renderer.link = (href, title, text) => { return `${text}`; }; marked.setOptions({ renderer: renderer }); } // 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; } async importTweetArchive(userID, tweetArchive) { 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(imageData); // } catch (e) { // console.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) { 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("Service worker already registered."); return registrations[0]; } navigator.serviceWorker .register("/sw.js") .then((registration) => { console.log("Service Worker registered with scope:", registration.scope); return registration; }) .catch((error) => { console.error("Service Worker registration failed:", error); }); } addPost(userID, postText, mediaData, mediaType) { if ((typeof postText !== "string") || postText.length === 0) { log("Not posting an empty string..."); return; } 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.render(); } getPeerID() { let id = localStorage.getItem("peer_id"); if (!id) { id = generateID(); localStorage.setItem("peer_id", id); } return id; } getUserID() { let id = localStorage.getItem("dandelion_id"); if (!id) { 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) { // Event listener for going offline window.addEventListener('offline', () => { log("offline"); }); // Event listener for going online window.addEventListener('online', async () => { log("online"); connection.connect(); this.render(); }); 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); }); } 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"); let clearPostsButton = document.getElementById("clear_posts"); let updateApp = document.getElementById("update_app"); let ddlnLogoButton = document.getElementById('ddln_logo_button'); // let addPic = document.getElementById('button_add_pic') as HTMLDivElement; let filePickerLabel = document.getElementById('file_input_label'); let filePicker = document.getElementById('file_input'); let toggleDark = document.getElementById('toggle_dark'); toggleDark.addEventListener('click', () => { document.documentElement.style.setProperty('--main-bg-color', 'white'); document.documentElement.style.setProperty('--main-fg-color', 'black'); }); filePicker?.addEventListener('change', async (event) => { for (let file of filePicker.files) { let buffer = await file.arrayBuffer(); let type = this.addPost(this.userID, 'image...', buffer); } }); filePickerLabel?.addEventListener('click', () => { console.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(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(); let type = this.addPost(this.userID, 'image...', buffer); }); postButton.addEventListener("click", () => { this.addPost(userID, postText.value); postText.value = ""; }); updateApp.addEventListener("click", () => { registration?.active?.postMessage({ type: "update_app" }); }); let infoElement = document.getElementById('info'); if (infoElement === null) { return; } ddlnLogoButton.addEventListener('click', () => { infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none'; }); } async loadPosts(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) { 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 purgeEmptyUsers() { let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', '')); if (!knownUsers) { return; } for (let userID of knownUsers) { let ids = await getAllIds(userID); if (ids.length === 0) { console.log(`Purging user ${userID}`); indexedDB.deleteDatabase(`user_${userID}`); } console.log(`https://ddln.app/user/${userID}`); } } async main() { this.isHeadless = /\bHeadlessChrome\//.test(navigator.userAgent); let userID = this.getUserID(); let peerID = this.getPeerID(); this.userID = userID; this.peerID = peerID; this.getRoute(); if (this.router.route === App.Route.CONNECT) { console.log('connect', this.router.userID); localStorage.setItem("dandelion_id", this.router.userID); } let urlParams = (new URL(window.location.href)).searchParams; let connection_userID = urlParams.get('connect'); let registration = undefined; if (urlParams.has('log')) { document.getElementById('info').style.display = "block"; this.showLog = true; } let time = 0; let delta = 0; // let isPersisted = await navigator?.storage?.persisted(); // if (!isPersisted) { // debugger; // const isPersisted = await navigator.storage.persist(); // 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(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) { 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") { registration = await this.registerServiceWorker(); // } this.username = this.getUsername(); document.getElementById('username').innerText = `${this.username}`; this.peername = this.getPeername(); 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(userID, this.posts, registration); let connectURL = `https://${document.location.hostname}/connect/${this.userID}`; document.getElementById('connectURL').innerHTML = `connect`; let qrcode = await new QRCode(document.getElementById('qrcode'), { text: connectURL, width: 256, height: 256, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.H }); document.querySelector('#qrcode > img').classList.add('qrcode_image'); document.querySelector('#qrcode > canvas').classList.add('qrcode_image'); log(`username:${this.username} user:${userID} peer:${peerID}`); await this.purgeEmptyUsers(); let websocket = new wsConnection(userID, peerID); window.addEventListener('beforeunload', () => { websocket.disconnect(); }); this.initOffline(websocket); // 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')); // }) } computeDiff(newPosts) { // return {added, deleted, same} } async render() { if (this.isHeadless) { console.log('Headless so skipping render...'); return; } this.timerStart(); let existingPosts = this.posts; this.posts = []; switch (this.router.route) { case App.Route.HOME: case App.Route.CONNECT: { this.posts = await this.loadPosts(this.userID) ?? []; break; } case App.Route.USER: { this.posts = await this.loadPosts(this.router.userID) ?? []; break; } case App.Route.POST: { this.posts = await this.loadPosts(this.router.userID, this.router.postID) ?? []; break; } default: { console.log("Render: got a route I didn't understand. Rendering HOME:", this.router.route); this.posts = await this.loadPosts(this.userID) ?? []; break; } } 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("added:", addedPosts, "removed:", deletedPosts); const fragment = document.createDocumentFragment(); let contentDiv = document.getElementById("content"); if (!contentDiv) { throw new Error(); } contentDiv.innerHTML = ""; // let count = 0; for (let i = this.posts.length - 1; i >= 0; i--) { let postData = this.posts[i]; // this.postsSet.add(postData); // return promises for all image loads and await those. let post = this.renderPost(postData); if (post) { fragment.appendChild(post); // count++; } // if (count > 100) { // break; // } } if (!contentDiv) { throw new Error("Couldn't get content div!"); } contentDiv.appendChild(fragment); let renderTime = this.timerDelta(); log(`render took: ${renderTime.toFixed(2)}ms`); if (performance?.memory) { log(`memory used: ${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`); } } async deletePost(userID, postID) { deleteData(userID, postID); this.render(); } renderPost(post) { 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 = `https://${document.location.hostname}/user/${post.author_id}/post/${post.post_id}`; await navigator.clipboard.writeText(shareUrl); }; let ownPost = post.author_id === this.userID; let postTemplate = `

@${post.author} - ${post.post_timestamp.toLocaleDateString()} ${ownPost ? `` : ''} ${ownPost ? `` : ''}
${marked.parse(post.text)}
`; 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() { app.router.userID = this.userID; let path = document.location.pathname; console.log(">>>>>>>>>>>>>>>>>>>>>>>>>", 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(">>>>>>>>>>>>>", this.router, 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 = {})); ; })(App || (App = {})); let app = new App(); window.addEventListener("load", app.main.bind(app)); //# sourceMappingURL=main.js.map