{
return this.sync.getFollowing(userID);
}
async loadPostsFromStorage(userID: string, postID?: string) {
this.timerStart();
let posts: StoragePost[] = [];
if (postID) {
const post = await getPostForUser(userID, postID);
posts = post ? [post] : [];
} else {
posts = await getData(userID, new Date(2022, 8), new Date());
}
if (posts?.length) {
console.log.apply(null, log(`Loaded ${posts.length} posts in ${this.timerDelta().toFixed(2)}ms`));;
return posts;
}
console.log.apply(null, log(`No posts found for userID:${userID}, postID:${postID}`));;
// 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 = this.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(globalThis.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}`);
this.statusBar.setHeadless(this.isHeadless);
let limitPostsParam = urlParams.get('limitPosts');
if (limitPostsParam) {
this.limitPosts = parseInt(limitPostsParam);
}
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.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.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
// }
let registration;
let shouldRegisterServiceWorker = !(this.isBootstrapPeer || this.isArchivePeer || this.isHeadless);
if (shouldRegisterServiceWorker) {
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);
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();
}
renderComposeReplyArea(replyToID:string) {
let composeReplyArea = document.getElementById('compose-reply-area') as HTMLElement;
composeReplyArea.innerText = replyToID;
composeReplyArea.classList.add("show");
}
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 replyButton = document.createElement('button'); replyButton.innerText = 'reply';
replyButton.onclick = async () => {
console.log(`replying to post ${post.post_id}`);
this.enterCompose(post.post_id);
// 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("