Mucho mucho update

This commit is contained in:
bobbydigitales
2024-11-19 19:14:00 -08:00
parent 6c692c4b9f
commit 71b5284b0f
11 changed files with 564 additions and 213 deletions

View File

@@ -27,20 +27,48 @@ type DBError = Event & {
};
function upgrade_0to1(db: IDBDatabase) {
async function upgrade_0to1(db: IDBDatabase) {
let postsStore = db.createObjectStore(postStoreName, { keyPath: "id", autoIncrement: true });
postsStore.createIndex("datetimeIndex", "post_timestamp", { unique: false });
postsStore.createIndex("postIDIndex", "data.post_id", { unique: true });
}
function upgrade_1to2(db: IDBDatabase) {
async function upgrade_1to2(db: IDBDatabase) {
console.log("Upgrading database from 1 to 2");
console.log("Converting all image arraybuffers to Blobs")
// TODO convert all images from arraybuffers to blobs
let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
if (knownUsers.length === 0) {
return;
}
for (const userID of knownUsers as string[]) {
console.log(`Converting images for user ${userID}`)
let posts = await getAllData(userID);
for (const post of posts) {
let image_data = post.data.image_data;
if (image_data) {
let blob = new Blob([image_data as ArrayBuffer]);
post.data.image_data = blob;
// TODO get the format of a new post right
addData(userID, post);
}
}
}
}
async function upgrade_2to3(db: IDBDatabase) {
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
}
let upgrades = new Map([
[0, upgrade_0to1],
[1, upgrade_1to2]
[1, upgrade_1to2],
[2, upgrade_2to3]
]);
export function openDatabase(userID: string): Promise<IDBDatabase> {
@@ -54,14 +82,15 @@ export function openDatabase(userID: string): Promise<IDBDatabase> {
reject(`Database error: ${errorEvent.target.error?.message}`);
};
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
request.onupgradeneeded = async (event: IDBVersionChangeEvent) => {
const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
let upgradeFunction = upgrades.get(event.oldVersion);
if (!upgradeFunction) {
throw new Error(`db: Don't have an upgrade function to go from version ${event.oldVersion} to version ${event.newVersion}`);
}
upgradeFunction(db);
debugger;
await upgradeFunction(db);
};
request.onsuccess = (event: Event) => {

View File

@@ -16,13 +16,13 @@ Problems
user
posts
media
tombstones
following
profile
name
description
profile pic
media
tombstones
following
profile
name
description
profile pic
Restruucture the app around the data. App/WS split is messy. Clean it up.
@@ -31,7 +31,7 @@ Restruucture the app around the data. App/WS split is messy. Clean it up.
// import * as ForceGraph3D from "3d-force-graph";
import { getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds } from "db";
import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds } from "db";
// declare let WebTorrent: any;
@@ -126,7 +126,7 @@ function logID(ID: string) {
// }
let logLines: string[] = [];
let logLength = 10;
let logLength = 30;
let logVisible = false;
function renderLog() {
if (!logVisible) {
@@ -142,7 +142,7 @@ function renderLog() {
function log(message: string) {
console.log(message);
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
if (logLines.length > 10) {
if (logLines.length > logLength) {
logLines = logLines.slice(logLines.length - logLength);
}
@@ -244,21 +244,10 @@ async function arrayBufferToBase64(buffer: ArrayBuffer) {
return (await bytesToBase64DataUrl(bytes) as string).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: string) {
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: string) {
@@ -326,6 +315,15 @@ class wsConnection {
this.connect();
}
// So we don't need custom logic everywhere we use this, I just wrapped it.
shouldSyncUserID(userID: string) {
if (app.isHeadless) {
return true;
}
return this.UserIDsToSync.has(userID);
}
async send(message: any) {
let json = ""
try {
@@ -542,7 +540,7 @@ class wsConnection {
// TODO only get users you're following here. ✅
let knownUsers = [...(await indexedDB.databases())].map(db => db.name?.replace('user_', '')).filter(userID => userID !== undefined);
knownUsers = knownUsers
.filter(userID => this.UserIDsToSync.has(userID))
.filter(userID => this.shouldSyncUserID(userID))
.filter(userID => !this.userBlockList.has(userID))
.filter(async userID => (await getAllIds(userID)).length > 0); // TODO getting all the IDs is unecessary, replace it with a test to get a single ID.
@@ -567,7 +565,7 @@ class wsConnection {
let getAllUsers = app.router.route !== App.Route.USER
if (getAllUsers) {
users = [...users, ...Object.entries(data.userPeers).filter(userID => this.UserIDsToSync.has(userID[0]))];
users = [...users, ...Object.entries(data.userPeers).filter(userID => this.shouldSyncUserID(userID[0]))];
}
// log(`Net: got ${users.length} users from bootstrap peer. \n${users.map((user)=>user[0]).join('\n')}`)
@@ -687,6 +685,7 @@ class App {
vizGraph: any | null = null;
qrcode: any = null;
connectURL: string = "";
firstRun = false;
getPreferentialUserID() {
return this.router.userID.length !== 0 ? this.router.userID : this.userID;
@@ -759,6 +758,27 @@ class App {
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' });
@@ -772,6 +792,10 @@ class App {
window.URL.revokeObjectURL(url);
}
async importPostsForUser(userID: string, posts: string) {
}
async exportPostsForUser(userID: string) {
let posts = await getAllData(userID);
@@ -789,7 +813,18 @@ class App {
output.push(newPost);
}
this.downloadJson(output, `ddln_${this.username}_export`);
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[]) {
@@ -1168,22 +1203,29 @@ class App {
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 importTweetsButton = document.getElementById("import_tweets") as HTMLButtonElement;
let exportButton = document.getElementById("export_button") 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 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 monitorButton = document.getElementById('monitor_button') as HTMLDivElement;
let composeButton = document.getElementById('compose_button') as HTMLDivElement;
// let addPic = document.getElementById('button_add_pic') as HTMLDivElement;
let filePickerLabel = document.getElementById('file_input_label');
let filePicker = document.getElementById('file_input') as HTMLInputElement;
let toggleDark = document.getElementById('toggle_dark') as HTMLButtonElement;
// let toggleDark = document.getElementById('toggle_dark') as HTMLButtonElement;
exportButton.addEventListener('click', async e => await this.exportPostsForUser(this.userID));
toggleDark.addEventListener('click', () => {
document.documentElement.style.setProperty('--main-bg-color', 'white');
document.documentElement.style.setProperty('--main-fg-color', 'black');
})
// toggleDark.addEventListener('click', () => {
// document.documentElement.style.setProperty('--main-bg-color', 'white');
// document.documentElement.style.setProperty('--main-fg-color', 'black');
// })
composeButton.addEventListener('click', e => {
document.getElementById('compose')!.style.display = 'block';
document.getElementById('textarea_post')?.focus();
});
filePicker?.addEventListener('change', async (event: any) => {
@@ -1207,27 +1249,27 @@ class App {
localStorage.setItem("dandelion_username", this.username);
})
importTweetsButton.addEventListener('click', async () => {
let file = await this.selectFile('text/*');
// importTweetsButton.addEventListener('click', async () => {
// let file = await this.selectFile('text/*');
console.log(file);
if (file == null) {
return;
}
// 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 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();
// 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() });
// clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() });
let postButton = document.getElementById("button_post") as HTMLButtonElement;
@@ -1247,14 +1289,19 @@ class App {
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" });
});
// updateApp.addEventListener("click", () => {
// registration?.active?.postMessage({ type: "update_app" });
// });
ddlnLogoButton.addEventListener('click', async () => {
// ddlnLogoButton.addEventListener('click', async () => {
// this.showInfo()
// });
monitorButton.addEventListener('click', async () => {
this.showInfo()
});
}
@@ -1305,7 +1352,7 @@ class App {
this.timerStart();
let posts: StoragePost[] = [];
// if (postID) {
// if (postID) {
// posts = await gePostForUser(userID, postID);
// }
@@ -1323,7 +1370,7 @@ class App {
// return await getData(userID, new Date(2022, 8), new Date());
}
async purgeEmptyUsers() {
async listUsers() {
let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
if (knownUsers.length === 0) {
return;
@@ -1331,18 +1378,19 @@ class App {
let preferredId = app.getPreferentialUserID()
for (let userID of knownUsers as string[]) {
if (userID === preferredId) {
continue;
}
// if (userID === preferredId) {
// continue;
// }
let ids = await getAllIds(userID);
if (ids.length === 0) {
console.log(`Purging user ${userID}`);
indexedDB.deleteDatabase(`user_${userID}`);
continue;
}
// let ids = await getAllIds(userID);
// if (ids.length === 0) {
// console.log(`Purging user ${userID}`);
// indexedDB.deleteDatabase(`user_${userID}`);
// continue;
// }
console.log(`https://ddln.app/user/${userID}`);
// console.log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID);
}
}
@@ -1456,8 +1504,13 @@ class App {
}
async initDB() {
let db = await openDatabase(this.userID);
}
async main() {
// await this.exportPostsForUser('b38b623c-c3fa-4351-9cab-50233c99fa4e');
// Get initial state and route from URL and user agent etc
@@ -1471,7 +1524,8 @@ class App {
// Start the process of figuring out what posts we need
// Download posts once all current images are loaded
window.resizeTo(645, 900);
// window.resizeTo(645, 900);
// this.initLogo()
@@ -1488,6 +1542,7 @@ class App {
this.peername = this.getPeername();
this.userID = this.getUserID();
this.username = this.getUsername();
await this.initDB();
this.connectURL = `https://${document.location.hostname}/connect/${this.userID}`;
document.getElementById('connectURL')!.innerHTML = `<a href="${this.connectURL}">connect</a>`;
@@ -1554,7 +1609,7 @@ class App {
log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`);
await this.purgeEmptyUsers();
// await this.purgeEmptyUsers();
let IDsToSync = this.following;
if (this.router.route === App.Route.USER) {
@@ -1564,6 +1619,9 @@ class App {
this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync);
this.initOffline(this.websocket);
// this.listUsers()
// this.createNetworkViz();
// const client = new WebTorrent()
@@ -1584,8 +1642,11 @@ class App {
}
renderWelcome(contentDiv: HTMLDivElement) {
contentDiv.innerHTML = `<div style="font-size:32px">Doing complicated shennanigans to load posts for you so just hang on a minute, ok!?</div>`;
contentDiv.innerHTML = `<div style="font-size:24px">
Welcome to Dandelion v0.1!<br>
Loading posts for the default feed...
</div>
`;
}
// keep a map of posts to dom nodes.
@@ -1616,11 +1677,11 @@ class App {
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";
// let compose = document.getElementById('compose');
// if (!compose) {
// break;
// }
// compose.style.display = "block";
break;
}
case App.Route.USER: {
@@ -1653,7 +1714,7 @@ class App {
throw new Error();
}
if (this.posts.length === 0) {
this.renderWelcome(contentDiv);
this.renderWelcome(contentDiv as HTMLDivElement);
return;
}
@@ -1683,12 +1744,13 @@ class App {
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);
// return promises for all image loads and await those.
let post = this.renderPost(postData.data);
// 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);
@@ -1726,7 +1788,7 @@ class App {
this.render();
}
renderPost(post: Post) {
renderPost(post: Post, first: boolean) {
if (!(post.hasOwnProperty("text"))) {
throw new Error("Post is malformed!");
}
@@ -1737,7 +1799,7 @@ class App {
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 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}`;
@@ -1765,7 +1827,7 @@ class App {
let userURL = `https://${document.location.hostname}/user/${post.author_id}/`
let postTemplate =
`<div><hr>
`<div>${first ? '' : '<hr>'}
<div>
<span class='header' title='${timestamp}'><img class="logo" src="/static/favicon.ico"><a class="username" href="${userURL}">@${post.author}</a> -
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
@@ -1782,7 +1844,7 @@ class App {
if (ownPost) {
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
containerDiv.querySelector('#editButton')?.appendChild(editButton);
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
}
@@ -1807,7 +1869,7 @@ class App {
image.src = url;
// image.src = image.src = "data:image/png;base64," + post.image;
image.className = "postImage";
image.onclick = () => { App.maximizeElement(image) };
// image.onclick = () => { App.maximizeElement(image) };
containerDiv.appendChild(image);
// containerDiv.appendChild(timestampDiv);