Mucho mucho update
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
sudo /root/.deno/bin/deno run --inspect --allow-all --unstable-temporal server.ts
|
||||
sudo /root/.deno/bin/deno run --inspect --allow-all --unstable-temporal --watch server.ts
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
// Use Deno static serving for static ✅
|
||||
// Use Workers, at least for serving static files. Why not nginx? Single binary server.
|
||||
|
||||
import { serveDir } from "jsr:@std/http/file-server"
|
||||
// import { serveDir } from "jsr:@std/http/file-server"
|
||||
|
||||
const memoryCache = false;
|
||||
const memoryResponseMap: Map<string, Response> = new Map();
|
||||
|
||||
const memoryCache = true;
|
||||
const filepathResponseCache: Map<string, Response> = new Map();
|
||||
// deno-lint-ignore-file prefer-const no-explicit-any
|
||||
async function serveFile(filename: string) {
|
||||
// console.log(filename)
|
||||
|
||||
if (!memoryCache) {
|
||||
const file = await Deno.readFile("../" + filename);
|
||||
const newResponse = new Response(file);
|
||||
@@ -22,9 +21,10 @@ async function serveFile(filename: string) {
|
||||
return newResponse;
|
||||
}
|
||||
|
||||
const response = memoryResponseMap.get(filename);
|
||||
const response = filepathResponseCache.get(filename);
|
||||
|
||||
if (response) {
|
||||
// console.log('serveFile: cache hit:', filename);
|
||||
return response.clone();
|
||||
}
|
||||
|
||||
@@ -36,15 +36,14 @@ async function serveFile(filename: string) {
|
||||
}
|
||||
|
||||
console.log(`Caching: ${filename}`);
|
||||
memoryResponseMap.set(filename, newResponse);
|
||||
filepathResponseCache.set(filename, newResponse);
|
||||
|
||||
return newResponse.clone();
|
||||
}
|
||||
|
||||
function hashIdToNumber(id: string, range: number) {
|
||||
let number = 0;
|
||||
let hash = 0x811c9dc5
|
||||
for (let char of id) {
|
||||
for (const char of id) {
|
||||
if (char !== '0' && char !== '-') {
|
||||
hash ^= char.charCodeAt(0);
|
||||
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
||||
@@ -72,7 +71,7 @@ function colorID(id: string) {
|
||||
|
||||
|
||||
function pingHandler(m: any) {
|
||||
let time = Temporal.Now.zonedDateTimeISO();
|
||||
const time = Temporal.Now.zonedDateTimeISO();
|
||||
// console.log("ping", m);
|
||||
console.log(time, `ping handler ${colorID(m.peer_id)}:${m.peer_name} ${colorID(m.user_id)}:${m.user_name}`);
|
||||
return '{"type":"pong"}'
|
||||
@@ -87,10 +86,21 @@ interface HelloMessage {
|
||||
known_users: string[]
|
||||
}
|
||||
|
||||
|
||||
// interface PeerState {
|
||||
// socket:WebSocket;
|
||||
// lastSeen: number;
|
||||
// }
|
||||
|
||||
// const peerStates:Map<string, PeerState> = new Map();
|
||||
const userPeers: Map<string, Set<string>> = new Map();
|
||||
const peerSockets: Map<string, WebSocket> = new Map();
|
||||
const socketPeers: Map<WebSocket, string> = new Map();
|
||||
|
||||
// function updatePeerState(peerID:string, socket:WebSocket) {
|
||||
|
||||
// }
|
||||
|
||||
function helloHandler(m: HelloMessage, socket: WebSocket) {
|
||||
console.log(`Received hello from peer ${colorID(m.peer_id)}:${m.peer_name}, user ${colorID(m.user_id)}:${m.user_name}`);
|
||||
|
||||
@@ -116,9 +126,9 @@ function helloHandler(m: HelloMessage, socket: WebSocket) {
|
||||
|
||||
|
||||
|
||||
let returnValue: any = {};
|
||||
for (let key of userPeers.keys()) {
|
||||
let peers = userPeers.get(key);
|
||||
const returnValue: any = {};
|
||||
for (const key of userPeers.keys()) {
|
||||
const peers = userPeers.get(key);
|
||||
if (!peers || peers.size === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -170,8 +180,8 @@ function peerMessageHandler(m: PeerMessage, _socket: WebSocket) {
|
||||
const messageDispatch: Map<string, (m: any, socket: WebSocket) => string | null> = new Map();
|
||||
|
||||
function deletePeerFromUserPeers(peerIDToDelete: string) {
|
||||
for (let [userID, peers] of userPeers.entries()) {
|
||||
for (let peerID of peers) {
|
||||
for (const peers of userPeers.values()) {
|
||||
for (const peerID of peers) {
|
||||
if (peerID === peerIDToDelete) {
|
||||
peers.delete(peerIDToDelete);
|
||||
}
|
||||
@@ -185,7 +195,7 @@ function connectWebsocket(request: Request) {
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(request);
|
||||
socket.addEventListener("open", (event) => {
|
||||
socket.addEventListener("open", () => {
|
||||
console.log("New peer websocket connection");
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
@@ -214,7 +224,7 @@ function connectWebsocket(request: Request) {
|
||||
});
|
||||
|
||||
socket.addEventListener("close", (event: CloseEvent) => {
|
||||
let peerID = socketPeers.get(socket);
|
||||
const peerID = socketPeers.get(socket);
|
||||
if (!peerID) {
|
||||
console.log("Websocket close: couldn't find peer 🤔");
|
||||
return;
|
||||
@@ -229,12 +239,26 @@ function connectWebsocket(request: Request) {
|
||||
|
||||
}
|
||||
|
||||
function handler(request: Request, info: any) {
|
||||
async function devServerWatchFiles() {
|
||||
const watcher = Deno.watchFs("../static/");
|
||||
for await (const event of watcher) {
|
||||
if (event.kind === "modify") {
|
||||
for (const path of event.paths) {
|
||||
const cachedPath = path.replace(Deno.cwd() + '/..', '')
|
||||
filepathResponseCache.delete(cachedPath);
|
||||
console.log('Purging updated file:', cachedPath)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handler(request: Request, info: any): Promise<Response> | Response {
|
||||
if (request.url === "https://ddln.app/") {
|
||||
return serveFile("/static/index.html")
|
||||
}
|
||||
|
||||
console.log(info.remoteAddr.hostname, request.url, request.headers.get('user-agent'));
|
||||
// console.log(info.remoteAddr.hostname, request.url, request.headers.get('user-agent'));
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
@@ -267,13 +291,12 @@ function handler(request: Request, info: any) {
|
||||
|
||||
if (url.pathname.includes("/static/")) {
|
||||
return serveFile(url.pathname);
|
||||
// return serveDir(request, { fsRoot: "../" });
|
||||
}
|
||||
|
||||
return serveFile("/static/index.html")
|
||||
}
|
||||
|
||||
function main() {
|
||||
async function main() {
|
||||
|
||||
messageDispatch.set('ping', pingHandler);
|
||||
messageDispatch.set('hello', helloHandler);
|
||||
@@ -284,6 +307,8 @@ function main() {
|
||||
cert: Deno.readTextFileSync("/etc/letsencrypt/live/ddlion.net/fullchain.pem"),
|
||||
key: Deno.readTextFileSync("/etc/letsencrypt/live/ddlion.net/privkey.pem"),
|
||||
}, handler);
|
||||
|
||||
await devServerWatchFiles();
|
||||
}
|
||||
|
||||
main();
|
||||
await main();
|
||||
39
src/db.ts
39
src/db.ts
@@ -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) => {
|
||||
|
||||
226
src/main.ts
226
src/main.ts
@@ -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);
|
||||
|
||||
34
static/db.js
34
static/db.js
@@ -10,17 +10,40 @@ const followingStoreName = "following";
|
||||
let keyBase = "dandelion_posts_v1_";
|
||||
let key = "";
|
||||
let version = 1;
|
||||
function upgrade_0to1(db) {
|
||||
async function upgrade_0to1(db) {
|
||||
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) {
|
||||
async function upgrade_1to2(db) {
|
||||
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) {
|
||||
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]);
|
||||
post.data.image_data = blob;
|
||||
// TODO get the format of a new post right
|
||||
addData(userID, post);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async function upgrade_2to3(db) {
|
||||
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) {
|
||||
const dbName = `user_${userID}`;
|
||||
@@ -30,13 +53,14 @@ export function openDatabase(userID) {
|
||||
const errorEvent = event;
|
||||
reject(`Database error: ${errorEvent.target.error?.message}`);
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
request.onupgradeneeded = async (event) => {
|
||||
const db = event.target.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) => {
|
||||
const db = event.target.result;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -14,9 +14,17 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script defer type="module" src="/static/main.js"></script>
|
||||
<script defer src="/static/lib/marked.min.js"></script>
|
||||
<script defer src="/static/lib/qrcode.min.js"></script>
|
||||
|
||||
<link rel="preload" href="/static/main.css" as="style">
|
||||
<link rel="preload" href="/static/main.js" as="script">
|
||||
<link rel="preload" href="/static/lib/marked.min.js" as="script">
|
||||
<link rel="preload" href="/static/lib/qrcode.min.js" as="script">
|
||||
|
||||
<link rel="stylesheet" href="/static/main.css">
|
||||
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
<script src="/static/lib/marked.min.js"></script>
|
||||
<script src="/static/lib/qrcode.min.js"></script>
|
||||
|
||||
<!-- <script src="/static/lib/3d-force-graph.js"></script> -->
|
||||
<!-- <script src="lib/lottie.min.js"></script> -->
|
||||
@@ -24,17 +32,51 @@
|
||||
|
||||
<!-- <script src="/lib/webtorrent/webtorrent_1_8_0.min.js"></script> -->
|
||||
<link rel="manifest" href="/static/app.webmanifest">
|
||||
<link defer rel="stylesheet" href="/static/main.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<button class="burger-menu">☰</button>
|
||||
<div class="nav-overlay"></div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="content">
|
||||
<div class="img-button" id="ddln_logo_button"></div>
|
||||
|
||||
<nav class="nav-container">
|
||||
<a class="nav-item profile-pic">
|
||||
<span class="nav-profile" role="img" aria-label="Home"><img class="profile-pic-container"
|
||||
src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:wobbjvmqx65vou5ipgpgs3wf/bafkreigbpq2unrhkxgisofimgqmiiiplp2ssmdgw6cqsa7i2ogtvlxqzfa@jpeg"></span>
|
||||
</a>
|
||||
<a class="nav-item">
|
||||
<span class="nav-emoji emoji-fill" role="img" aria-label="Home">🏠</span>
|
||||
<span class="nav-label">Home</span>
|
||||
</a>
|
||||
<!-- <a class="nav-item">
|
||||
<span class="nav-emoji" role="img" aria-label="Search">🔍</span>
|
||||
<span class="nav-label">Search</span>
|
||||
</a>
|
||||
<a class="nav-item">
|
||||
<span class="nav-emoji" role="img" aria-label="Notifications">🔔</span>
|
||||
<span class="nav-label">Notifications</span>
|
||||
</a> -->
|
||||
<a class="nav-item">
|
||||
<span class="nav-emoji emoji-fill" role="img" aria-label="Profile">👤</span>
|
||||
<span class="nav-label">Profile</span>
|
||||
</a>
|
||||
<a class="nav-item">
|
||||
<span class="nav-emoji emoji-fill" role="img" aria-label="Settings">⚙️</span>
|
||||
<span class="nav-label">Settings</span>
|
||||
</a>
|
||||
<a id="monitor_button" class="nav-item">
|
||||
<span class="nav-emoji" role="img" aria-label="Monitor">🤓</span>
|
||||
<span class="nav-label">Monitor</span>
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<div class="content">
|
||||
<!-- <div class="img-button" id="ddln_logo_button"></div> -->
|
||||
<div id="status"></div>
|
||||
|
||||
<div id="info" style="display:none">
|
||||
@@ -65,11 +107,11 @@
|
||||
<div id="buttons">
|
||||
<!-- <button id="button_font1" >font1</button>
|
||||
<button id="button_font2" >font2 </button> -->
|
||||
<button id="import_tweets">import</button>
|
||||
<!-- <button id="import_tweets">import</button> -->
|
||||
<button id="export_button">export</button>
|
||||
<button id="clear_posts">clear </button>
|
||||
<button id="update_app">check for updates</button>
|
||||
<button id="toggle_dark">light/dark</button>
|
||||
<!-- <button id="clear_posts">clear </button> -->
|
||||
<!-- <button id="update_app">check for updates</button> -->
|
||||
<!-- <button id="toggle_dark">light/dark</button> -->
|
||||
</div>
|
||||
|
||||
<textarea cols="60" rows="6" id="textarea_post"></textarea>
|
||||
@@ -83,6 +125,9 @@
|
||||
</div>
|
||||
<!-- <div id="torrent-content"></div> -->
|
||||
<div id="content"></div>
|
||||
|
||||
<div id="compose_button" class="compose-button emoji-fill">✏️</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
152
static/main.css
152
static/main.css
@@ -2,9 +2,6 @@
|
||||
/* styles to apply if a user's device settings are set to reduced motion */
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
:root {
|
||||
--main-bg-color: white;
|
||||
--border-color: rgb(132, 136, 138);
|
||||
@@ -25,9 +22,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
|
||||
font-family: sans-serif;
|
||||
color: var(--main-fg-color);
|
||||
background-color: var(--main-bg-color);
|
||||
@@ -73,7 +68,6 @@ hr {
|
||||
align-items: flex-start;
|
||||
min-height: 100vh;
|
||||
padding: 0px;
|
||||
/* Add some padding around the flex container */
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -107,7 +101,6 @@ hr {
|
||||
text-wrap: nowrap;
|
||||
font-size: 10px;
|
||||
margin-bottom: 20px;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -131,7 +124,7 @@ a {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
image-rendering: pixelated;
|
||||
background-image: url('/static/favicon.ico');
|
||||
background-image: url("/static/favicon.ico");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
@@ -141,7 +134,7 @@ a {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
image-rendering: pixelated;
|
||||
background-image: url('/static/favicon.ico');
|
||||
background-image: url("/static/favicon.ico");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
@@ -151,7 +144,7 @@ a {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
image-rendering: pixelated;
|
||||
background-image: url('/static/favicon.ico');
|
||||
background-image: url("/static/favicon.ico");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
@@ -187,7 +180,7 @@ button,
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
iframe {
|
||||
@@ -210,7 +203,7 @@ iframe {
|
||||
}
|
||||
|
||||
#compose {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
|
||||
.username {
|
||||
@@ -220,4 +213,137 @@ iframe {
|
||||
|
||||
.username:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
position: sticky;
|
||||
/* left: 0; */
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
background-color: var(--main-bg-color);
|
||||
border-right: 1px solid rgb(60, 60, 60);
|
||||
/* transition: width 0.3s; */
|
||||
overflow: hidden;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.nav-container.collapsed {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
color: var(--main-fg-color);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-item.profile-pic {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: var(--edge-color);
|
||||
}
|
||||
|
||||
.nav-emoji {
|
||||
font-size: 28px;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.burger-menu {
|
||||
display: none;
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 16px;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--main-fg-color);
|
||||
}
|
||||
|
||||
.nav-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.nav-container {
|
||||
transform: translateX(-100%);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.nav-container.active {
|
||||
transform: translateX(0);
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.burger-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-overlay.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 701px) and (max-width: 800px) {
|
||||
.nav-container {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-pic-container {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profile-pic-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.emoji-fill {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 var(--main-fg-color);
|
||||
}
|
||||
|
||||
.compose-button {
|
||||
font-size: 48px;
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
background: rgba(87, 87, 255, 0.8);
|
||||
}
|
||||
|
||||
.compose-button:hover {
|
||||
background-color: rgb(87, 87, 255);
|
||||
}
|
||||
|
||||
182
static/main.js
182
static/main.js
@@ -26,7 +26,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, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds } from "db";
|
||||
// let posts:any;
|
||||
// let keyBase = "dandelion_posts_v1_"
|
||||
// let key:string = "";
|
||||
@@ -97,7 +97,7 @@ function logID(ID) {
|
||||
// log?.appendChild(newlog);
|
||||
// }
|
||||
let logLines = [];
|
||||
let logLength = 10;
|
||||
let logLength = 30;
|
||||
let logVisible = false;
|
||||
function renderLog() {
|
||||
if (!logVisible) {
|
||||
@@ -112,7 +112,7 @@ function renderLog() {
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
|
||||
if (logLines.length > 10) {
|
||||
if (logLines.length > logLength) {
|
||||
logLines = logLines.slice(logLines.length - logLength);
|
||||
}
|
||||
renderLog();
|
||||
@@ -171,20 +171,10 @@ 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
|
||||
@@ -249,6 +239,13 @@ class wsConnection {
|
||||
window.addEventListener('beforeunload', () => this.disconnect());
|
||||
this.connect();
|
||||
}
|
||||
// So we don't need custom logic everywhere we use this, I just wrapped it.
|
||||
shouldSyncUserID(userID) {
|
||||
if (app.isHeadless) {
|
||||
return true;
|
||||
}
|
||||
return this.UserIDsToSync.has(userID);
|
||||
}
|
||||
async send(message) {
|
||||
let json = "";
|
||||
try {
|
||||
@@ -386,7 +383,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.
|
||||
console.log('Net: Sending known users', knownUsers.map(userID => logID(userID ?? "")));
|
||||
@@ -407,7 +404,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')}`)
|
||||
for (let [userID, peerIDs] of users) {
|
||||
@@ -511,6 +508,7 @@ class App {
|
||||
this.vizGraph = null;
|
||||
this.qrcode = null;
|
||||
this.connectURL = "";
|
||||
this.firstRun = 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'];
|
||||
@@ -579,6 +577,22 @@ class App {
|
||||
}
|
||||
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' });
|
||||
@@ -591,6 +605,8 @@ class App {
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
async importPostsForUser(userID, posts) {
|
||||
}
|
||||
async exportPostsForUser(userID) {
|
||||
let posts = await getAllData(userID);
|
||||
let output = [];
|
||||
@@ -602,7 +618,10 @@ 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, tweetArchive) {
|
||||
log("Importing tweet archive");
|
||||
@@ -890,19 +909,25 @@ class App {
|
||||
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 importTweetsButton = document.getElementById("import_tweets") as HTMLButtonElement;
|
||||
let exportButton = document.getElementById("export_button");
|
||||
let clearPostsButton = document.getElementById("clear_posts");
|
||||
let updateApp = document.getElementById("update_app");
|
||||
let ddlnLogoButton = document.getElementById('ddln_logo_button');
|
||||
// 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');
|
||||
let composeButton = document.getElementById('compose_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');
|
||||
// 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) => {
|
||||
for (let file of filePicker.files) {
|
||||
@@ -920,22 +945,22 @@ class App {
|
||||
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(); });
|
||||
// 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)) {
|
||||
@@ -950,11 +975,15 @@ 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" });
|
||||
});
|
||||
ddlnLogoButton.addEventListener('click', async () => {
|
||||
// updateApp.addEventListener("click", () => {
|
||||
// registration?.active?.postMessage({ type: "update_app" });
|
||||
// });
|
||||
// ddlnLogoButton.addEventListener('click', async () => {
|
||||
// this.showInfo()
|
||||
// });
|
||||
monitorButton.addEventListener('click', async () => {
|
||||
this.showInfo();
|
||||
});
|
||||
}
|
||||
@@ -994,7 +1023,7 @@ class App {
|
||||
async loadPostsFromStorage(userID, postID) {
|
||||
this.timerStart();
|
||||
let posts = [];
|
||||
// if (postID) {
|
||||
// if (postID) {
|
||||
// posts = await gePostForUser(userID, postID);
|
||||
// }
|
||||
posts = await getData(userID, new Date(2022, 8), new Date());
|
||||
@@ -1007,23 +1036,24 @@ class App {
|
||||
// addDataArray(userID, posts);
|
||||
// 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;
|
||||
}
|
||||
let preferredId = app.getPreferentialUserID();
|
||||
for (let userID of knownUsers) {
|
||||
if (userID === preferredId) {
|
||||
continue;
|
||||
}
|
||||
let ids = await getAllIds(userID);
|
||||
if (ids.length === 0) {
|
||||
console.log(`Purging user ${userID}`);
|
||||
indexedDB.deleteDatabase(`user_${userID}`);
|
||||
continue;
|
||||
}
|
||||
// if (userID === preferredId) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// createLogoCanvas() {
|
||||
@@ -1109,6 +1139,9 @@ class App {
|
||||
// .enableNavigationControls(false);
|
||||
console.log(`create viz network took ${this.timerDelta()}ms`);
|
||||
}
|
||||
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
|
||||
@@ -1118,7 +1151,7 @@ class App {
|
||||
// Load all images async
|
||||
// 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()
|
||||
this.isHeadless = /\bHeadlessChrome\//.test(navigator.userAgent);
|
||||
this.getRoute();
|
||||
@@ -1131,6 +1164,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>`;
|
||||
let urlParams = (new URL(window.location.href)).searchParams;
|
||||
@@ -1177,13 +1211,14 @@ class App {
|
||||
document.getElementById('peer_id').innerText = `peer_id:${this.peerID}`;
|
||||
this.initButtons(this.userID, this.posts, registration);
|
||||
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) {
|
||||
IDsToSync = new Set([this.router.userID]);
|
||||
}
|
||||
this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync);
|
||||
this.initOffline(this.websocket);
|
||||
// this.listUsers()
|
||||
// this.createNetworkViz();
|
||||
// const client = new WebTorrent()
|
||||
// // Sintel, a free, Creative Commons movie
|
||||
@@ -1199,7 +1234,11 @@ class App {
|
||||
// })
|
||||
}
|
||||
renderWelcome(contentDiv) {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
async render() {
|
||||
if (this.isHeadless) {
|
||||
@@ -1216,11 +1255,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: {
|
||||
@@ -1274,11 +1313,13 @@ class App {
|
||||
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);
|
||||
// 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);
|
||||
@@ -1304,7 +1345,7 @@ class App {
|
||||
deleteData(userID, postID);
|
||||
this.render();
|
||||
}
|
||||
renderPost(post) {
|
||||
renderPost(post, first) {
|
||||
if (!(post.hasOwnProperty("text"))) {
|
||||
throw new Error("Post is malformed!");
|
||||
}
|
||||
@@ -1313,8 +1354,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 () => {
|
||||
@@ -1334,7 +1374,7 @@ class App {
|
||||
markdown = markdown.replace("<iframe", `<iframe style="width:100%;height:50px;display:none" onblur="this.style.display = 'inline';"`);
|
||||
}
|
||||
let userURL = `https://${document.location.hostname}/user/${post.author_id}/`;
|
||||
let postTemplate = `<div><hr>
|
||||
let postTemplate = `<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>
|
||||
@@ -1348,7 +1388,7 @@ class App {
|
||||
containerDiv.innerHTML = postTemplate;
|
||||
if (ownPost) {
|
||||
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
|
||||
containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
||||
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
||||
}
|
||||
containerDiv.querySelector('#shareButton')?.appendChild(shareButton);
|
||||
if (!("image_data" in post && post.image_data)) {
|
||||
@@ -1367,7 +1407,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);
|
||||
return containerDiv;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user