import { getData, addData, addDataArray, clearData, deleteData } 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)); } let logLines = []; let logLength = 10; function 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); // You can perform your action here } }); // let connectionReply = await wsConnection.send('hello'); // for (let peeer 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); // } // } class wsConnection { 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 = (evt) => { log("ws:connected"); this.websocket.send(`{"type":"hello", "user_id": "${this.userID}", "peer_id":"${this.peerID}"}`); this.websocketPingInterval = window.setInterval(() => { if (!navigator.onLine) { return; } this.websocket.send(`{"type":"ping", "peer_id": "${this.peerID}"}`); }, 10000); }; this.websocket.onclose = (evt) => { 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:response: ' + event.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.userID = userID; this.peerID = peerID; this.connect(); if (!this.websocket) { // set a timer and retry? } } } // function connectWebsocket(userID: string) { // let websocket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`); // websocket.onopen = function (evt) { // log("Websocket: CONNECTED"); // websocket.send(`{"messageType":"connect", "id": "${userID}"}`); // let websocketPingInterval = window.setInterval(() => { websocket.send(`{"messageType":"ping", "id": "${userID}"}`); }, 5000) // }; // websocket.onclose = function (evt) { // log("Websocket: DISCONNECTED"); // }; // websocket.onmessage = function (evt) { // log('Websocket: RESPONSE: ' + evt.data); // }; // websocket.onerror = function (evt) { // log('Websocket: ERROR: ' + evt); // }; // return websocket; // } class App { constructor() { this.userID = ''; this.peerID = ''; this.time = 0; } initMarkdown() { const renderer = new marked.Renderer(); renderer.link = (href, title, text) => { return `${text}`; }; marked.setOptions({ renderer: renderer }); } arrayBufferToBase64(buffer) { return new Promise((resolve, reject) => { const blob = new Blob([buffer], { type: 'application/octet-stream' }); const reader = new FileReader(); reader.onloadend = () => { const dataUrl = reader.result; 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, posts, postText) { if ((typeof postText !== "string") || postText.length === 0) { log("Not posting an empty string..."); return; } let post = new Post(`bobbydigitales`, userID, postText, new Date()); posts.push(post); // localStorage.setItem(key, JSON.stringify(posts)); addData(userID, post); this.render(posts); } 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; } 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', () => { log("online"); connection.connect(); }); 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"); let font2Button = document.getElementById("button_font2"); 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'); font1Button.addEventListener('click', () => { this.setFont('Bookerly', '16px'); }); font2Button.addEventListener('click', () => { this.setFont('Virgil', '16px'); }); 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); posts = await this.loadPosts(userID) ?? []; this.render(posts); }); clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render(posts); }); let postButton = document.getElementById("button_post"); let postText = document.getElementById("textarea_post"); if (!(postButton && postText)) { throw new Error(); } postButton.addEventListener("click", () => { this.addPost(userID, posts, 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) { this.timerStart(); let 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 main() { let urlParams = (new URL(window.location.href)).searchParams; let connection_userID = urlParams.get('connect'); let registration = undefined; if (urlParams.get("sw") === "true") { registration = await this.registerServiceWorker(); } if (connection_userID) { console.log('connect', connection_userID); localStorage.setItem("dandelion_id", connection_userID); } let posts = []; let userID = this.getUserID(); let peerID = this.getPeerID(); this.userID = userID; this.peerID = peerID; log(`user:${userID} peer:${peerID}`); let websocket = new wsConnection(userID, peerID); window.addEventListener('beforeunload', () => { websocket.disconnect(); }); this.initOffline(websocket); this.initButtons(userID, posts, registration); let time = 0; let delta = 0; if (navigator.storage && navigator.storage.persist && !navigator.storage.persisted) { 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}); posts = await this.loadPosts(userID) ?? []; // debugger; this.timerStart(); this.render(posts); // , (postID:string)=>{this.deletePost(userID, postID)} 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`); } // 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')); // }) } render(posts) { const fragment = document.createDocumentFragment(); let contentDiv = document.getElementById("content"); if (!contentDiv) { throw new Error(); } contentDiv.innerHTML = ""; let count = 0; new QRCode(document.getElementById('qrcode'), `https://ddlion.net/?connect=${this.userID}`); for (let i = posts.length - 1; i >= 0; i--) { let postData = posts[i]; let post = this.renderPost(postData, posts); if (post) { fragment.appendChild(post); count++; } if (count > 100) { break; } } if (!contentDiv) { throw new Error("Couldn't get content div!"); } contentDiv.appendChild(fragment); } deletePost(userID, postID) { deleteData(userID, postID); } renderPost(post, posts) { 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 = ()=>{deletefunc(post.post_id)}; let postTemplate = `

@${post.author} - ${post.post_timestamp.toLocaleDateString()}
${marked.parse(post.text)}
`; containerDiv.innerHTML = postTemplate; containerDiv.appendChild(deleteButton); // if (!("image_data" in post && post.image_data)) { // containerDiv.appendChild(timestampDiv); // return containerDiv; // // return null; // } // let image = document.createElement("img"); // const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/jpg' }); // 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"; // containerDiv.appendChild(image); // containerDiv.appendChild(timestampDiv); return containerDiv; } } let app = new App(); window.addEventListener("load", app.main.bind(app)); //# sourceMappingURL=main.js.map