lotsa stuff

This commit is contained in:
bobbydigitales
2024-10-30 20:51:44 -07:00
parent 05c4800461
commit 6ce693cefc
13 changed files with 1421 additions and 373 deletions

View File

@@ -1,25 +1,37 @@
// TODO
// Peer mssages
// Routing
// Video files being fully sent
// Use Deno static serving for static
// TODO: server
// Peer mssages
// Routing
// Video files being fully sent
// 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"
// deno-lint-ignore-file prefer-const no-explicit-any
function serveFile(filename: string) {
// console.log(filename)
const responseText = Deno.readFileSync("../" + filename);
// console.log(responseText)
const response = new Response(responseText);
if (filename.endsWith('.js')) {
response.headers.set('content-type', 'application/javascript')
const memoryResponseMap: Map<string, Response> = new Map();
// deno-lint-ignore-file prefer-const no-explicit-any
async function serveFile(filename: string) {
// console.log(filename)
const response = memoryResponseMap.get(filename);
if (response) {
return response.clone();
}
return response;
const file = await Deno.readFile("../" + filename);
const newResponse = new Response(file);
if (filename.endsWith('.js')) {
newResponse.headers.set('content-type', 'application/javascript')
}
memoryResponseMap.set(filename, newResponse);
return newResponse.clone();
}
function hashIdToNumber(id: string, range:number) {
function hashIdToNumber(id: string, range: number) {
let number = 0;
let hash = 0x811c9dc5
for (let char of id) {
@@ -36,16 +48,23 @@ function hashIdToNumber(id: string, range:number) {
const colors = [
160, 196, 202, 208, 214, 220, 226, 190, 154, 118, 82, 46, 47, 48, 49,
51, 45, 44, 43, 42, 41, 40, 39, 33, 27, 21, 57, 93, 129, 165, 201,
];
];
const resetCode = "\x1b[0m";
function colorID(id) {
function colorID(id: string) {
if (typeof id !== 'string') {
console.error(`colorID: expected string but got `, id);
return "";
}
const colorCode = `\x1b[38;5;${colors[hashIdToNumber(id, colors.length)]}m`
return `${colorCode}${id.substring(0,5)}${resetCode}`
return `${colorCode}${id.substring(0, 5)}${resetCode}`
}
function pingHandler(m: any) {
console.log(colorID(m.peer_id), "pong handler", m);
let 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"}'
}
@@ -73,7 +92,7 @@ function helloHandler(m: HelloMessage, socket: WebSocket) {
peerSockets.set(m.peer_id, socket);
socketPeers.set(socket, m.peer_id);
if (Symbol.iterator in Object(m.known_users)) {
for (const knownUserID of m.known_users) {
console.log(`Adding user ${knownUserID} from peer ${colorID(m.peer_id)}`);
if (!userPeers.get(knownUserID)) {
@@ -82,11 +101,15 @@ function helloHandler(m: HelloMessage, socket: WebSocket) {
userPeers.get(knownUserID)?.add(m.peer_id);
}
}
let returnValue: any = {};
for (let key of userPeers.keys()) {
let peers = userPeers.get(key);
if (!peers) {
if (!peers || peers.size === 0) {
continue;
}
returnValue[key] = [...peers.keys()];
@@ -124,6 +147,7 @@ function peerMessageHandler(m: PeerMessage, _socket: WebSocket) {
if (toPeer.readyState !== WebSocket.OPEN) {
console.log("Peer socket is not open:", toPeer);
deletePeerFromUserPeers(m.to);
return null;
}
@@ -135,14 +159,24 @@ 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) {
if (peerID === peerIDToDelete) {
peers.delete(peerIDToDelete);
}
}
}
}
function connectWebsocket(request: Request) {
if (request.headers.get("upgrade") != "websocket") {
return new Response(null, { status: 501 });
}
const { socket, response } = Deno.upgradeWebSocket(request);
socket.addEventListener("open", () => {
console.log("a client connected!");
socket.addEventListener("open", (event) => {
console.log("New peer websocket connection");
});
socket.addEventListener("message", (event) => {
// console.log(event);
@@ -169,8 +203,16 @@ function connectWebsocket(request: Request) {
});
socket.addEventListener("close", (event) => {
socket.addEventListener("close", (event: CloseEvent) => {
let peerID = socketPeers.get(socket);
if (!peerID) {
console.log("Websocket close: couldn't find peer 🤔");
return;
}
console.log("Websocket close:", colorID(peerID), `code:${event.code} reason:${event.reason} wasClean: ${event.wasClean}`);
peerSockets.delete(peerID);
deletePeerFromUserPeers(peerID);
});
return response;
@@ -178,10 +220,19 @@ function connectWebsocket(request: Request) {
}
function handler(request: Request, info: any) {
if (request.url === "https://ddln.app/") {
return serveFile("/static/index.html")
}
// console.log(info.remoteAddr);
console.log(info.remoteAddr.hostname, request.url, request.headers.get('user-agent'));
const url = new URL(request.url);
if (url.pathname.endsWith('mp4') || url.pathname.endsWith('webm')) {
console.log("Not serving video...");
return new Response("Not serving video", { status: 404 });
}
if (url.pathname === "/") {
return serveFile("/static/index.html")
}
@@ -195,7 +246,7 @@ function handler(request: Request, info: any) {
return serveFile("static/sw.js")
}
if (url.pathname === "/robots).txt") {
if (url.pathname === "/robots.txt") {
return serveFile("static/robots.txt")
}

View File

@@ -5,9 +5,13 @@
// }
// Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917
const postStoreName: string = "posts";
const tombStoneStoreName: string = "tombstones"
const followingStoreName: string = "following"
let keyBase = "dandelion_posts_v1_"
let key = "";
let version = 1;
@@ -22,12 +26,22 @@ type DBError = Event & {
target: { errorCode: DOMException };
};
function upgrade_0to1(db:IDBDatabase) {
let store = db.createObjectStore(postStoreName, { keyPath: "id", autoIncrement: true });
store.createIndex("datetimeIndex", "post_timestamp", { unique: false });
store.createIndex("postIDIndex", "data.post_id", { unique: true });
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) {
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
}
let upgrades = new Map([
[0, upgrade_0to1],
[1, upgrade_1to2]
]);
export function openDatabase(userID: string): Promise<IDBDatabase> {
const dbName = `user_${userID}`
@@ -43,8 +57,11 @@ export function openDatabase(userID: string): Promise<IDBDatabase> {
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
upgrade_0to1(db);
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);
};
request.onsuccess = (event: Event) => {
@@ -54,17 +71,17 @@ export function openDatabase(userID: string): Promise<IDBDatabase> {
});
}
async function getDBTransactionStore(userID:string) {
async function getDBTransactionStore(userID: string, mode: IDBTransactionMode = "readonly") {
const db = await openDatabase(userID);
const transaction = db.transaction(postStoreName, "readwrite");
const transaction = db.transaction(postStoreName, mode);
const store = transaction.objectStore(postStoreName);
return {db, transaction, store}
return { db, transaction, store }
}
export async function addData(userID: string, data: any): Promise<void> {
try {
const {db, transaction, store} = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
const addRequest = store.add({ post_timestamp: data.post_timestamp, data: data });
addRequest.onsuccess = (e: Event) => {
@@ -83,7 +100,7 @@ export async function addData(userID: string, data: any): Promise<void> {
export async function deleteData(userID: string, postID: string) {
try {
const {db, transaction, store} = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
const index = store.index("postIDIndex");
const getRequest = index.getKey(postID);
@@ -108,7 +125,7 @@ export async function deleteData(userID: string, postID: string) {
export async function clearData(userID: string) {
try {
const {db, transaction, store} = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
const clearRequest = store.clear();
clearRequest.onsuccess = (e: Event) => {
@@ -128,25 +145,30 @@ export async function clearData(userID: string) {
export async function addDataArray(userID: string, array: any[]): Promise<void> {
try {
const {db, transaction, store} = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
let count = 0;
transaction.onerror = (event: Event) => {
console.error('Error in adding data:', event);
}
// let count = 0;
array.reverse();
for (let data of array) {
const addRequest = store.add({ post_timestamp: data.post_timestamp, data: data });
addRequest.onsuccess = (e: Event) => {
// console.log('Data has been added:', (e.target as IDBRequest).result);
};
// addRequest.onsuccess = (e: Event) => {
// // console.log('Data has been added:', (e.target as IDBRequest).result);
// };
addRequest.onerror = (event: Event) => {
// Use a type assertion to access the specific properties of IDBRequest error event
const errorEvent = event as IDBRequestEvent;
console.error('Error in adding data:', errorEvent.target.error?.message);
};
// addRequest.onerror = (event: Event) => {
// // Use a type assertion to access the specific properties of IDBRequest error event
// const errorEvent = event as IDBRequestEvent;
// console.error('Error in adding data:', errorEvent.target.error?.message);
// };
count++;
// count++;
// if (count % 100 === 0) {
// console.log(`Added ${count} posts...`);
@@ -161,7 +183,7 @@ export async function addDataArray(userID: string, array: any[]): Promise<void>
export async function checkPostIds(userID: string, post_ids: string[]) {
try {
const {db, transaction, store} = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID);
const index = store.index("postIDIndex");
@@ -213,7 +235,7 @@ export async function checkPostIds(userID: string, post_ids: string[]) {
export async function mergeDataArray(userID: string, array: any[]): Promise<void> {
try {
const {db, transaction, store} = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
const index = store.index("postIDIndex");
@@ -292,7 +314,7 @@ export async function getData(userID: string, lowerID: Date, upperID: Date): Pro
export async function getAllData(userID: string): Promise<any | undefined> {
const {store} = await getDBTransactionStore(userID);
const { store } = await getDBTransactionStore(userID);
return new Promise((resolve, reject) => {
const getRequest = store.getAll();
@@ -318,7 +340,7 @@ export async function getAllData(userID: string): Promise<any | undefined> {
}
export async function getAllIds(userID: string): Promise<any | undefined> {
const {store} = await getDBTransactionStore(userID);
const { store } = await getDBTransactionStore(userID);
const index = store.index("postIDIndex");
@@ -326,7 +348,7 @@ export async function getAllIds(userID: string): Promise<any | undefined> {
return new Promise((resolve, reject) => {
let request = index.openKeyCursor();
request.onsuccess = (event:any) => {
request.onsuccess = (event: any) => {
let cursor = event.target.result;
if (cursor) {
keys.push(cursor.key);
@@ -342,8 +364,8 @@ export async function getAllIds(userID: string): Promise<any | undefined> {
});
}
export async function getPostsByIds(userID:string, postIDs:string[]) {
const {store} = await getDBTransactionStore(userID);
export async function getPostsByIds(userID: string, postIDs: string[]) {
const { store } = await getDBTransactionStore(userID);
const index = store.index("postIDIndex");
let posts = [];
@@ -351,7 +373,7 @@ export async function getPostsByIds(userID:string, postIDs:string[]) {
const post = await new Promise((resolve, reject) => {
let request = index.get(postID);
request.onsuccess = (event:any) => {
request.onsuccess = (event: any) => {
resolve(event.target.result); // Resolve with the post
};

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ const contentToCache = [
'/static/main.js',
'/static/lib/marked.min.js',
'/static/lib/qrcode.min.js',
'/static/lib/d3.js',
'/static/db.js',
'/static/favicon.ico'
];

View File

@@ -7,6 +7,7 @@
"window-controls-overlay",
"standalone"
],
"id": "b1dbe643-36fc-4419-9448-80f32a1baa1a",
"background_color": "#000000",
"theme_color": "#000000",

View File

@@ -5,14 +5,23 @@
// }
// Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917
const postStoreName = "posts";
const tombStoneStoreName = "tombstones";
const followingStoreName = "following";
let keyBase = "dandelion_posts_v1_";
let key = "";
let version = 1;
function upgrade_0to1(db) {
let store = db.createObjectStore(postStoreName, { keyPath: "id", autoIncrement: true });
store.createIndex("datetimeIndex", "post_timestamp", { unique: false });
store.createIndex("postIDIndex", "data.post_id", { unique: true });
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) {
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
}
let upgrades = new Map([
[0, upgrade_0to1],
[1, upgrade_1to2]
]);
export function openDatabase(userID) {
const dbName = `user_${userID}`;
return new Promise((resolve, reject) => {
@@ -23,7 +32,11 @@ export function openDatabase(userID) {
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
upgrade_0to1(db);
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);
};
request.onsuccess = (event) => {
const db = event.target.result;
@@ -31,15 +44,15 @@ export function openDatabase(userID) {
};
});
}
async function getDBTransactionStore(userID) {
async function getDBTransactionStore(userID, mode = "readonly") {
const db = await openDatabase(userID);
const transaction = db.transaction(postStoreName, "readwrite");
const transaction = db.transaction(postStoreName, mode);
const store = transaction.objectStore(postStoreName);
return { db, transaction, store };
}
export async function addData(userID, data) {
try {
const { db, transaction, store } = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
const addRequest = store.add({ post_timestamp: data.post_timestamp, data: data });
addRequest.onsuccess = (e) => {
// console.log('Data has been added:', (e.target as IDBRequest).result);
@@ -56,7 +69,7 @@ export async function addData(userID, data) {
}
export async function deleteData(userID, postID) {
try {
const { db, transaction, store } = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
const index = store.index("postIDIndex");
const getRequest = index.getKey(postID);
getRequest.onerror = e => console.log(e.target.error);
@@ -77,7 +90,7 @@ export async function deleteData(userID, postID) {
}
export async function clearData(userID) {
try {
const { db, transaction, store } = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
const clearRequest = store.clear();
clearRequest.onsuccess = (e) => {
// console.log('Data has been added:', (e.target as IDBRequest).result);
@@ -94,20 +107,23 @@ export async function clearData(userID) {
}
export async function addDataArray(userID, array) {
try {
const { db, transaction, store } = await getDBTransactionStore(userID);
let count = 0;
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
transaction.onerror = (event) => {
console.error('Error in adding data:', event);
};
// let count = 0;
array.reverse();
for (let data of array) {
const addRequest = store.add({ post_timestamp: data.post_timestamp, data: data });
addRequest.onsuccess = (e) => {
// console.log('Data has been added:', (e.target as IDBRequest).result);
};
addRequest.onerror = (event) => {
// Use a type assertion to access the specific properties of IDBRequest error event
const errorEvent = event;
console.error('Error in adding data:', errorEvent.target.error?.message);
};
count++;
// addRequest.onsuccess = (e: Event) => {
// // console.log('Data has been added:', (e.target as IDBRequest).result);
// };
// addRequest.onerror = (event: Event) => {
// // Use a type assertion to access the specific properties of IDBRequest error event
// const errorEvent = event as IDBRequestEvent;
// console.error('Error in adding data:', errorEvent.target.error?.message);
// };
// count++;
// if (count % 100 === 0) {
// console.log(`Added ${count} posts...`);
// }
@@ -161,7 +177,7 @@ export async function checkPostIds(userID, post_ids) {
}
export async function mergeDataArray(userID, array) {
try {
const { db, transaction, store } = await getDBTransactionStore(userID);
const { db, transaction, store } = await getDBTransactionStore(userID, "readwrite");
const index = store.index("postIDIndex");
transaction.oncomplete = () => {
// console.log("Transaction completed successfully");

File diff suppressed because one or more lines are too long

View File

@@ -6,15 +6,25 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1">
<title>Dandelion</title>
<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 type="importmap">
{
"imports": {
"db": "/static/db.js"
}
}
</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>
<!-- <script src="/static/lib/3d-force-graph.js"></script> -->
<!-- <script src="lib/lottie.min.js"></script> -->
<!-- <script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script> -->
<!-- <script src="/lib/webtorrent/webtorrent_1_8_0.min.js"></script> -->
<link rel="manifest" href="/static/app.webmanifest">
<link rel="stylesheet" href="/static/main.css">
<link defer rel="stylesheet" href="/static/main.css">
</head>
<body>
@@ -23,14 +33,19 @@
<div class="flex-container">
<div class="content">
<div class="img-button" id="ddln_logo_button"><img class="logo" src="/static/favicon.ico"></div>
<div class="img-button" id="ddln_logo_button"></div>
<div id="status"></div>
<div id="info" style="display:none">
<div id="profile">
<span class="form_label">username:</span><span class="form_field" id="username" contenteditable="true">unnamed</span>
<div id="ids"><div id="user_id"></div><div id="peer_id"></div><div id="peername"></div></div>
<span class="form_label">username:</span><span class="form_field" id="username"
contenteditable="true">unnamed</span>
<div id="ids">
<div id="user_id"></div>
<div id="peer_id"></div>
<div id="peername"></div>
</div>
</div>
<!-- <div id="following">
@@ -38,28 +53,32 @@
<div>fiona</div>
</div> -->
<div id="peers"></div>
<div id="log" ></div>
<div id="log"></div>
<div id="connectURL"></div>
<div id="qrcode"></div>
<!-- <dotlottie-player src="https://lottie.host/272b60dd-462d-42a3-8ed6-fec4143633d6/X4FxBascRI.json" background="transparent" speed="1" style="width: 300px; height: 300px" direction="1" playMode="normal" loop controls autoplay></dotlottie-player> -->
<!-- <div><canvas id="peer_display"></canvas></div> -->
</div>
<!-- <div id="peer_display"><canvas></canvas></div> -->
<div id="compose">
<div id="buttons">
<!-- <button id="button_font1" >font1</button>
<button id="button_font2" >font2 </button> -->
<button id="import_tweets" >import</button>
<button id="clear_posts" >clear </button>
<button id="update_app" >check for updates</button>
<button id="toggle_dark" >light/dark</button>
<button id="import_tweets">import</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>
<div class="right">
<label for="file_input" id="file_input_label" class="button">photo</label>
<input type="file" id="file_input" multiple style="display:none">
<input type="file" id="file_input" accept="image/*" multiple style="display:none">
<!-- <button id="button_add_pic" >🏞️</button> -->
<button id="button_post" >post</button>
<button id="button_post">post</button>
</div>
</div>
<!-- <div id="torrent-content"></div> -->
<div id="content"></div>

View File

@@ -1,29 +1,29 @@
@media (prefers-reduced-motion) {
/* 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);
--edge-color:rgb(60,60,60);
--main-fg-color:black;
--highlight-fg-color:rgb(255,255,255);
--link-color:rgb(29, 155, 240);
--border-color: rgb(132, 136, 138);
--edge-color: rgb(60, 60, 60);
--main-fg-color: black;
--highlight-fg-color: rgb(255, 255, 255);
--link-color: rgb(29, 155, 240);
}
@media (prefers-color-scheme: dark) {
@media (prefers-color-scheme: dark) {
:root {
--main-bg-color: black;
--border-color:rgb(132,136,138);
--edge-color:rgb(60,60,60);
--main-fg-color:rgb(202,208,211);
--highlight-fg-color:rgb(255,255,255);
--link-color:rgb(29, 155, 240);
}
--border-color: rgb(132, 136, 138);
--edge-color: rgb(60, 60, 60);
--main-fg-color: rgb(202, 208, 211);
--highlight-fg-color: rgb(255, 255, 255);
--link-color: rgb(29, 155, 240);
}
}
body {
@@ -31,7 +31,7 @@ body {
font-family: sans-serif;
color: var(--main-fg-color);
background-color: var(--main-bg-color);
margin:0px;
margin: 0px;
}
hr {
@@ -108,7 +108,7 @@ hr {
font-size: 10px;
margin-bottom: 20px;
height: 150px;
width: 50%;
width: 100%;
}
.right {
@@ -127,10 +127,35 @@ a {
color: var(--link-color);
}
.logo {
#ddln_logo_button {
width: 32px;
height: 32px;
image-rendering: pixelated;
background-image: url('/static/favicon.ico');
background-repeat: no-repeat;
background-size: cover;
}
@media (prefers-reduced-motion) {
#ddln_logo_button {
width: 32px;
height: 32px;
image-rendering: pixelated;
background-image: url('/static/favicon.ico');
background-repeat: no-repeat;
background-size: cover;
}
}
@media (prefer) {
#ddln_logo_button {
width: 32px;
height: 32px;
image-rendering: pixelated;
background-image: url('/static/favicon.ico');
background-repeat: no-repeat;
background-size: cover;
}
}
#torrent-content {
@@ -142,8 +167,9 @@ a {
cursor: pointer;
}
button,.button {
font-size:small;
button,
.button {
font-size: small;
background-color: var(--main-bg-color);
border-radius: 10px;
padding-left: 10px;
@@ -159,11 +185,12 @@ button,.button {
}
video {
width:100%
width: 100%
}
iframe {
width: 100%
width: 100%;
/* display:none; */
}
.qrcode_image {
@@ -172,5 +199,23 @@ iframe {
}
#ids {
font-size:xx-small;
font-size: xx-small;
}
#peer_display {
width: 100%;
height: 300px;
}
#compose {
display: none
}
.username {
color: var(--main-fg-color);
text-decoration: none;
}
.username:hover {
text-decoration: underline;
}

View File

@@ -1,5 +1,32 @@
// TODO: virtual list, only rerender what's needed so things can keep playing.
import { getData, addData, addDataArray, clearData, deleteData, mergeDataArray, checkPostIds, getAllIds, getPostsByIds } from "./db.js";
/*
Problems
1. Can't delete, very annoying
Tombstones. Send all IDs and all Tombstones. Ask only for posts that we don't get a tombstone for. Don't send posts we have a tombstone for?
Posts don't propagate, you need to refresh to see new posts.
Broadcast when we post to all peers we know about.
3. Posting is slow because too long to render
2. Can't follow people
4. Can't like or reply to posts
user
posts
media
tombstones
following
profile
name
description
profile pic
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";
// let posts:any;
// let keyBase = "dandelion_posts_v1_"
// let key:string = "";
@@ -62,13 +89,19 @@ function uuidToBase58(uuid) {
function logID(ID) {
return ID.substring(0, 5);
}
// function log(message:string) {
// console.log(message);
// let log = document.getElementById("log");
// let newlog = document.createElement('span');
// newlog.innerHTML = `<pre>${message}</pre>`;
// log?.appendChild(newlog);
// }
let logLines = [];
let logLength = 10;
function log(message) {
console.log(message);
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
if (logLines.length > 10) {
logLines = logLines.slice(logLines.length - logLength);
let logVisible = false;
function renderLog() {
if (!logVisible) {
return;
}
let log = document.getElementById("log");
if (!log) {
@@ -76,6 +109,13 @@ function log(message) {
}
log.innerText = logLines.join("\n");
}
function log(message) {
console.log(message);
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
if (logLines.length > 10) {
logLines = logLines.slice(logLines.length - logLength);
}
}
function generateID() {
if (self.crypto.hasOwnProperty("randomUUID")) {
return self.crypto.randomUUID();
@@ -174,20 +214,29 @@ class wsConnection {
this.websocket.send(json);
}
helloResponseHandler(data) {
debugger;
let users = [];
let receivedUsers = Object.entries(data.userPeers);
log(`Net: got ${receivedUsers.length} users from bootstrap peer.`);
try {
let currentUserPeers = data.userPeers[app.router.userID];
users.push([app.router.userID, data.userPeers[app.router.userID]]);
delete data.userPeers[app.router.userID];
let preferentialID = app.getPreferentialID();
let currentUserPeers = data.userPeers[preferentialID];
users.push([preferentialID, currentUserPeers]);
delete data.userPeers[preferentialID];
}
catch (e) {
console.log('helloResponseHandler', e);
}
let getAllUsers = app.router.route !== App.Route.USER;
if (getAllUsers) {
users = [...users, ...Object.entries(data.userPeers)];
log(`Net: got ${users.length} users from bootstrap peer. ${users.join(',')}`);
}
// log(`Net: got ${users.length} users from bootstrap peer. \n${users.map((user)=>user[0]).join('\n')}`)
for (let [userID, peerIDs] of users) {
this.peers.set(userID, [...peerIDs]);
if (this.userBlockList.has(userID)) {
console.log("Skipping user on blocklist:", userID);
continue;
}
// this.peers.set(userID, [...peerIDs]);
for (let peerID of [...peerIDs]) {
if (peerID === this.peerID) {
continue;
@@ -209,29 +258,22 @@ class wsConnection {
async getPostIdsForUserResponseHandler(data) {
// log(`getPostsForUserResponse: ${data}`)
let message = data.message;
log(`Net: got ${message.post_ids.length} post IDs for user ${logID(data.message.user_id)} from peer ${logID(data.from)}`);
// console.log(`Checking post IDs...`);
let postIds = await checkPostIds(message.user_id, data.message.post_ids);
log(`Net: got ${message.post_ids.length} post IDs for user ${logID(message.user_id)} from peer ${logID(data.from)}`);
let startTime = app.timerStart();
let postIds = await checkPostIds(message.user_id, message.post_ids);
log(`ID Check for user ${logID(message.user_id)} took ${app.timerDelta().toFixed(2)}ms`);
log(`Need ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`);
if (postIds.length === 0) {
log(`Don't need any posts for user ${logID(data.message.user_id)} from peer ${logID(data.from)}`);
return;
}
log(`Net: Req ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`);
let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user", post_ids: postIds, user_id: message.user_id } };
this.send(responseMessage);
}
// static async compressArrayBuffer(data: ArrayBuffer): Promise<ArrayBuffer> {
// const compressionStream = new CompressionStream('gzip'); // You can also use 'deflate', 'deflate-raw', etc.
// const compressedStream = new Response(
// new Blob([data]).stream().pipeThrough(compressionStream)
// );
// const compressedArrayBuffer = await compressedStream.arrayBuffer();
// return compressedArrayBuffer;
// }
async getPostIdsForUserHandler(data) {
debugger;
let message = data.message;
let postIds = await getAllIds(message.user_id) ?? [];
postIds = postIds.filter((postID) => !this.postBlockList.has(postID));
if (postIds.length === 0) {
log(`Net: I know about user ${logID(message.user_id)} but I have 0 posts, so I'm not sending any to to peer ${logID(data.from)}`);
return;
@@ -240,6 +282,31 @@ class wsConnection {
let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_post_ids_for_user_response", post_ids: postIds, user_id: message.user_id } };
this.send(responseMessage);
}
async broadcastNewPost(userID, post) {
let newPost = { ...post };
if (post.image_data) {
newPost.image_data = await arrayBufferToBase64(post.image_data);
}
for (let [peerID, peerInfo] of this.seenPeers.entries()) {
log(`broadcastNewPost: sending new post to ${logID(peerID)}:${peerInfo.peerName}:${peerInfo.userName}`);
this.sendPostsForUser(peerID, app.userID, [newPost]);
}
}
async sendPostsForUser(toPeerID, userID, posts) {
let responseMessage = {
type: "peer_message",
from: app.peerID,
to: toPeerID,
from_username: app.username,
from_peername: app.peername,
message: {
type: "get_posts_for_user_response",
posts: posts,
user_id: userID
}
};
return this.send(responseMessage);
}
// Send posts to peer
async getPostsForUserHandler(data) {
let message = data.message;
@@ -247,23 +314,25 @@ class wsConnection {
log(`Net: Sending ${posts.length} posts for user ${logID(message.user_id)} to peer ${logID(data.from)}`);
app.timerStart();
let output = [];
console.log("Serializing images");
for (let post of posts) {
let newPost = post.data;
if (newPost.image_data) {
// let compressedData = await wsConnection.compressArrayBuffer(newPost.image_data);
// console.log((newPost.image_data.byteLength - compressedData.byteLength) / 1024 / 1024);
// TODO don't do this, use Blobs direclty!
// https://developer.chrome.com/blog/blob-support-for-Indexeddb-landed-on-chrome-dev
newPost.image_data = await arrayBufferToBase64(newPost.image_data);
}
// let megs = JSON.stringify(newPost).length/1024/1024;
// console.log(`getPostsForUserHandler id:${newPost.post_id} post length:${megs}`);
output.push(newPost);
}
// posts = posts.map((post:any)=>{let newPost = post.data; if (newPost.image_data){newPost.image_data = arraybufferto};return newPost});
// posts = posts.map((post:any)=>{})
let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user_response", posts: output, user_id: message.user_id } };
this.send(responseMessage);
console.log("Sending posts");
await this.sendPostsForUser(data.from, message.user_id, output);
let sendTime = app.timerDelta();
log(`send took: ${sendTime.toFixed(2)}ms`);
log(`getPostsForUserHandler send took: ${sendTime.toFixed(2)}ms`);
}
// Got posts from peer
async getPostsForUserReponseHandler(data) {
@@ -271,6 +340,13 @@ class wsConnection {
let message = data.message;
console.log(`Net: got ${message.posts.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`);
for (let post of message.posts) {
// HACK: Some posts have insanely large images, so I'm gonna skip them.
// If we supported delete then we we could delete these posts in a sensible way.
if (this.postBlockList.has(post.post_id)) {
log(`Skipping blocked post: ${post.post_id}`);
continue;
}
// HACK - some posts had the wrong author ID
if (message.user_id === app.userID) {
post.author_id = app.userID;
}
@@ -282,13 +358,14 @@ class wsConnection {
console.log(`Merging same user peer posts...`);
await mergeDataArray(message.user_id, data.message.posts);
let receiveTime = app.timerDelta();
log(`Receive took: ${receiveTime.toFixed(2)}ms`);
if (message.user_id === app.router.userID) {
log(`getPostsForUserReponseHandler receive took: ${receiveTime.toFixed(2)}ms`);
if (message.user_id === app.getPreferentialID()) {
app.render();
}
}
async peerMessageHandler(data) {
// log(`peerMessageHandler ${JSON.stringify(data)}`)
this.seenPeers.set(data.from, { peerName: data.from_peername, userName: data.from_username });
let peerMessageType = data.message.type;
let handler = this.peerMessageHandlers.get(peerMessageType);
if (!handler) {
@@ -297,6 +374,13 @@ class wsConnection {
}
handler(data);
}
async sendHello() {
let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
knownUsers = knownUsers.filter((userID) => userID && !this.userBlockList.has(userID));
knownUsers = knownUsers.filter(async (userID) => userID && (await getAllIds(userID)).length > 0);
console.log('Net: Sending known users', knownUsers.map(userID => logID(userID ?? "")));
return await this.send({ type: "hello", user_id: this.userID, user_name: app.username, peer_id: this.peerID, peer_name: app.peername, known_users: knownUsers });
}
connect() {
if (this.websocket?.readyState === WebSocket.OPEN) {
return;
@@ -315,14 +399,24 @@ class wsConnection {
}
this.websocket.onopen = async (event) => {
log("ws:connected");
let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
console.log('Net: Sending known users', knownUsers);
this.send({ type: "hello", user_id: this.userID, user_name: app.username, peer_id: this.peerID, peer_name: app.peername, known_users: knownUsers });
await this.sendHello();
// If we're running as a headless peer, send a hello message every 60 seconds to refresh the posts we have.
let helloRefreshIntervalPeriod = 120;
if (app.isHeadless) {
console.log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod);
this.helloRefreshInterval = window.setInterval(() => {
console.log("wsConnection: Hello refresh.");
if (!navigator.onLine) {
return;
}
this.sendHello();
}, helloRefreshIntervalPeriod * 1000);
}
this.websocketPingInterval = window.setInterval(() => {
if (!navigator.onLine) {
return;
}
this.send({ type: "ping", peer_id: this.peerID });
this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username });
}, 10000);
};
this.websocket.onclose = (event) => {
@@ -333,7 +427,6 @@ class wsConnection {
};
this.websocket.onmessage = (event) => {
// log('ws:<-' + event.data.slice(0, 240));
debugger;
let data = JSON.parse(event.data);
let { type } = data;
let handler = this.messageHandlers.get(type);
@@ -355,11 +448,35 @@ class wsConnection {
this.userID = "";
this.peerID = "";
this.websocketPingInterval = 0;
this.helloRefreshInterval = 0;
this.retry = 10;
this.state = 'disconnected';
this.peers = new Map();
// peers: Map<string, string[]> = new Map();
this.messageHandlers = new Map();
this.peerMessageHandlers = new Map();
this.seenPeers = new Map();
// static async compressArrayBuffer(data: ArrayBuffer): Promise<ArrayBuffer> {
// const compressionStream = new CompressionStream('gzip'); // You can also use 'deflate', 'deflate-raw', etc.
// const compressedStream = new Response(
// new Blob([data]).stream().pipeThrough(compressionStream)
// );
// const compressedArrayBuffer = await compressedStream.arrayBuffer();
// return compressedArrayBuffer;
// }
this.postBlockList = new Set([
'1c71f53c-c467-48e4-bc8c-39005b37c0d5',
'64203497-f77b-40d6-9e76-34d17372e72a',
'243130d8-4a41-471e-8898-5075f1bd7aec',
'e01eff89-5100-4b35-af4c-1c1bcb007dd0',
'194696a2-d850-4bb0-98f7-47416b3d1662',
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
'dd1d92aa-aa24-4166-a925-94ba072a9048'
]);
this.userBlockList = new Set([
'5d63f0b2-a842-41bf-bf06-e0e4f6369271',
'5f1b85c4-b14c-454c-8df1-2cacc93f8a77',
'bba3ad24-9181-4e22-90c8-c265c80873ea'
]);
this.userID = userID;
this.peerID = peerID;
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
@@ -381,10 +498,16 @@ class App {
this.peername = '';
this.userID = '';
this.peerID = '';
this.following = [];
this.following = new Set();
this.posts = [];
this.isHeadless = false;
this.showLog = false;
this.markedAvailable = false;
this.limitPosts = 50;
this.websocket = null;
this.vizGraph = null;
this.qrcode = null;
this.connectURL = "";
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'];
@@ -401,12 +524,19 @@ class App {
mediaID: ''
};
}
getPreferentialID() {
return this.router.userID.length !== 0 ? this.router.userID : this.userID;
}
initMarkdown() {
if (typeof marked === "undefined") {
return;
}
const renderer = new marked.Renderer();
renderer.link = (href, title, text) => {
return `<a href="${href}" target="_blank"${title ? ` title="${title}"` : ''}>${text}</a>`;
};
marked.setOptions({ renderer: renderer });
this.markedAvailable = true;
}
// arrayBufferToBase64(buffer: ArrayBuffer) {
// return new Promise((resolve, reject) => {
@@ -446,6 +576,20 @@ class App {
}
return fullText;
}
async exportPostsForUser(userID) {
let posts = await getAllData(userID);
let output = [];
console.log("Serializing images");
for (let post of posts) {
let newPost = post.data;
if (newPost.image_data) {
newPost.image_data = await arrayBufferToBase64(newPost.image_data);
}
output.push(newPost);
}
let json = JSON.stringify(output);
console.log(json);
}
async importTweetArchive(userID, tweetArchive) {
log("Importing tweet archive");
let postsTestData = [];
@@ -515,20 +659,83 @@ class App {
console.error("Service Worker registration failed:", error);
});
}
addPost(userID, postText, mediaData, mediaType) {
async compressImage(imageData, mimeType, quality = 0.5) {
let uncompressedByteLength = imageData.byteLength;
log(`compressImage input:${mimeType} size:${(uncompressedByteLength / 1024).toFixed(2)}KBi quality:${quality}`);
try {
// Convert ArrayBuffer to Blob
const blob = new Blob([imageData], { type: mimeType });
const bitmap = await createImageBitmap(blob, {
imageOrientation: 'none',
// resizeWidth: desiredWidth,
// resizeHeight: desiredHeight,
// resizeQuality: 'high',
});
// const bitmap = await createImageBitmap(bitmapTemp, {
// imageOrientation: 'none',
// resizeWidth: 600,
// resizeHeight: 800,
// // resizeHeight: (bitmapTemp.height / bitmapTemp.width) * 600,
// resizeQuality: 'high',
// })
//drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
// Create a canvas and draw the image onto it
// let scale = 1/32;
// let scaledWidth = bitmap.width*scale;
// let scaledHeight = bitmap.height*scale;
// let scale = 1/32;
let scaledWidth = bitmap.width;
let scaledHeight = bitmap.height;
let resizeThreshold = 600;
if (scaledWidth > resizeThreshold) {
scaledWidth = resizeThreshold;
scaledHeight = (bitmap.height / bitmap.width) * resizeThreshold;
}
const canvas = document.createElement('canvas');
canvas.width = scaledWidth;
canvas.height = scaledHeight;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
canvas.getContext('2d').drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, scaledWidth, scaledHeight);
// Compress the image and get the result as an ArrayBuffer
const compressedBlob = await new Promise((resolve, reject) => canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('Compression failed.'))), 'image/jpeg', quality));
// TODO: Don't need to do this as we'll be storing blobs directly.
let compressedArrayBuffer = await compressedBlob.arrayBuffer();
let compressedByteLength = compressedArrayBuffer.byteLength;
let percent = (uncompressedByteLength / compressedByteLength);
log(`compressImage: compressedSize:${(compressedArrayBuffer.byteLength / 1024).toFixed(2)}KBi ${percent.toFixed(2)}:1 compression`);
return compressedArrayBuffer;
}
catch (e) {
console.error(e);
return null;
}
}
async createNewPost(userID, postText, mediaData, mimeType) {
if ((typeof postText !== "string") || postText.length === 0) {
log("Not posting an empty string...");
return;
}
if (mediaData &&
(mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
mediaData.byteLength > 500 * 1024) {
let compressedImage = await this.compressImage(mediaData, mimeType, 0.9);
if (compressedImage) {
mediaData = compressedImage;
}
}
let post = new Post(this.username, userID, postText, new Date(), mediaData);
// this.posts.push(post);
// localStorage.setItem(key, JSON.stringify(posts));
addData(userID, post);
this.websocket?.broadcastNewPost(userID, post);
this.render();
}
getPeerID() {
let id = localStorage.getItem("peer_id");
if (!id) {
log(`Didn't find a peer ID, generating one`);
id = generateID();
localStorage.setItem("peer_id", id);
}
@@ -537,6 +744,7 @@ class App {
getUserID() {
let id = localStorage.getItem("dandelion_id");
if (!id) {
log(`Didn't find a user ID, generating one`);
id = generateID();
localStorage.setItem("dandelion_id", id);
}
@@ -657,8 +865,10 @@ class App {
filePicker?.addEventListener('change', async (event) => {
for (let file of filePicker.files) {
let buffer = await file.arrayBuffer();
let type = this.addPost(this.userID, 'image...', buffer);
await this.createNewPost(this.userID, 'image...', buffer, file.type);
}
// Reset so that if they pick the same image again, we still get the change event.
filePicker.value = '';
});
filePickerLabel?.addEventListener('click', () => {
console.log("Add pic...");
@@ -693,10 +903,10 @@ class App {
const dataTransfer = e.clipboardData;
const file = dataTransfer.files[0];
let buffer = await file.arrayBuffer();
let type = this.addPost(this.userID, 'image...', buffer);
await this.createNewPost(this.userID, 'image...', buffer, file.type);
});
postButton.addEventListener("click", () => {
this.addPost(userID, postText.value);
this.createNewPost(userID, postText.value);
postText.value = "";
});
updateApp.addEventListener("click", () => {
@@ -706,9 +916,53 @@ class App {
if (infoElement === null) {
return;
}
ddlnLogoButton.addEventListener('click', () => { infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none'; });
ddlnLogoButton.addEventListener('click', async () => {
infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none';
logVisible = infoElement.style.display == 'block';
renderLog();
if (this.qrcode != null) {
return;
}
async loadPosts(userID, postID) {
this.qrcode = await new QRCode(document.getElementById('qrcode'), {
text: this.connectURL,
width: 150,
height: 150,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
document.querySelector('#qrcode > img').classList.add('qrcode_image');
document.querySelector('#qrcode > canvas').classList.add('qrcode_image');
});
}
async getPostsForFeed() {
// get N posts from each user and sort them by date.
// This isn't really going to work very well.
// Eventually we'll need a db that only has followed user posts so we can get them chronologically
//
let posts = [];
for (let followedID of this.following.keys()) {
posts = posts.concat(await getData(followedID, new Date(2022, 8), new Date()));
// console.log(followedID);
}
// @ts-ignore
posts = posts.sort((a, b) => a.post_timestamp - b.post_timestamp);
return posts;
}
async loadFollowersFromStorage(userID) {
if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') {
return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e',
'6d774268-16cd-4e86-8bbe-847a0328893d', // Sean
'05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
'bba3ad24-9181-4e22-90c8-c265c80873ea', // Harry
'8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona
];
}
return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :)
}
async loadPostsFromStorage(userID, postID) {
this.timerStart();
let posts = [];
// if (postID) {
@@ -726,36 +980,140 @@ class App {
}
async purgeEmptyUsers() {
let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
if (!knownUsers) {
if (knownUsers.length === 0) {
return;
}
let preferredId = app.getPreferentialID();
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;
}
console.log(`https://ddln.app/user/${userID}`);
}
}
// createLogoCanvas() {
// let logoElement = document.getElementById('ddln_logo_button');
// if (!logoElement) {
// return;
// }
// let canvas = document.createElement('canvas');
// canvas.width = 32;
// canvas.height = 32;
// logoElement.appendChild(canvas);
// let ctx = canvas.getContext('2d');
// if (!ctx) {
// return;
// }
// if (ctx) {
// ctx.fillStyle = "red"; // Set fill color to red
// ctx.fillRect(0, 0, canvas.width, canvas.height); // Fill entire canvas
// }
// return canvas;
// }
// initLogo() {
// const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
// if (prefersReducedMotion.matches) {
// return;
// }
// // let canvas = this.createLogoCanvas();
// // if (!canvas) {
// // return;
// // }
// prefersReducedMotion.addEventListener("change", event => {
// let preferReducedMotion = event.matches == true;
// if (preferReducedMotion) {
// // canvas.remove();
// return;
// }
// // this.createLogoCanvas();
// });
// }
createNetworkViz() {
let timer = this.timerStart();
this.vizGraph = ForceGraph3D();
let vizElement = document.getElementById('ddln_logo_button');
if (!vizElement) {
return;
}
const N = 8;
const gData = {
nodes: [{ id: 0 }],
links: []
};
this.vizGraph(vizElement)
.graphData(gData)
// .width(32)
// .height(32)
.width(600)
.height(300)
.showNavInfo(false)
.nodeRelSize(8)
.nodeOpacity(1.0)
.linkWidth(2)
.linkOpacity(1.0)
.backgroundColor('rgba(0,0,0,0)');
let angle = 0;
const distance = 400;
setInterval(() => {
const { nodes, links } = this.vizGraph.graphData();
const id = nodes.length;
this.vizGraph.graphData({
nodes: [...nodes, { id }],
links: [...links, { source: id, target: Math.round(Math.random() * (id - 1)) }]
});
}, 1000);
setInterval(() => {
this.vizGraph.cameraPosition({
x: distance * Math.sin(angle),
z: distance * Math.cos(angle)
});
angle += Math.PI / 300;
}, 10);
// .backgroundColor('rgba(0,0,0,0)');
// .enablePointerInteraction(false)
// .enableNavigationControls(false);
console.log(`create viz network took ${this.timerDelta()}ms`);
}
async main() {
// await this.exportPostsForUser('bba3ad24-9181-4e22-90c8-c265c80873ea');
// Get initial state and route from URL and user agent etc
// Set local state (userid etc) based on that.
// Init libraries
// Render
// 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);
// this.initLogo()
this.isHeadless = /\bHeadlessChrome\//.test(navigator.userAgent);
let userID = this.getUserID();
let peerID = this.getPeerID();
this.userID = userID;
this.peerID = peerID;
this.getRoute();
if (this.router.route === App.Route.CONNECT) {
console.log('connect', this.router.userID);
localStorage.setItem("dandelion_id", this.router.userID);
localStorage.removeItem("dandelion_username");
}
this.peerID = this.getPeerID();
this.peername = this.getPeername();
this.userID = this.getUserID();
this.username = this.getUsername();
let urlParams = (new URL(window.location.href)).searchParams;
let connection_userID = urlParams.get('connect');
let registration = undefined;
if (urlParams.has('log')) {
document.getElementById('info').style.display = "block";
this.showLog = true;
}
if (urlParams.has('headless')) {
this.isHeadless = true;
}
let limitPostsParam = urlParams.get('limitPosts');
if (limitPostsParam) {
this.limitPosts = parseInt(limitPostsParam);
}
let time = 0;
let delta = 0;
// let isPersisted = await navigator?.storage?.persisted();
@@ -780,32 +1138,22 @@ class App {
// let storageUsed = (await navigator?.storage?.estimate())?.usage/1024/1024
// }
// if (urlParams.get("sw") === "true") {
let registration;
registration = await this.registerServiceWorker();
// }
this.username = this.getUsername();
document.getElementById('username').innerText = `${this.username}`;
this.peername = this.getPeername();
document.getElementById('peername').innerText = `peername:${this.peername}`;
document.getElementById('user_id').innerText = `user_id:${this.userID}`;
document.getElementById('peer_id').innerText = `peer_id:${this.peerID}`;
this.initButtons(userID, this.posts, registration);
let connectURL = `https://${document.location.hostname}/connect/${this.userID}`;
document.getElementById('connectURL').innerHTML = `<a href="${connectURL}">connect</a>`;
let qrcode = await new QRCode(document.getElementById('qrcode'), {
text: connectURL,
width: 256,
height: 256,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
document.querySelector('#qrcode > img').classList.add('qrcode_image');
document.querySelector('#qrcode > canvas').classList.add('qrcode_image');
log(`username:${this.username} user:${userID} peer:${peerID}`);
this.initButtons(this.userID, this.posts, registration);
this.connectURL = `https://${document.location.hostname}/connect/${this.userID}`;
document.getElementById('connectURL').innerHTML = `<a href="${this.connectURL}">connect</a>`;
log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`);
await this.purgeEmptyUsers();
let websocket = new wsConnection(userID, peerID);
window.addEventListener('beforeunload', () => { websocket.disconnect(); });
this.initOffline(websocket);
this.websocket = new wsConnection(this.userID, this.peerID);
window.addEventListener('beforeunload', () => { this.websocket?.disconnect(); });
this.initOffline(this.websocket);
// 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';
@@ -819,71 +1167,92 @@ class App {
// file.appendTo(document.getElementById('torrent-content'));
// })
}
computeDiff(newPosts) {
// return {added, deleted, same}
}
async render() {
if (this.isHeadless) {
console.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.posts = await this.loadPosts(this.userID) ?? [];
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.loadPosts(this.router.userID) ?? [];
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.loadPosts(this.router.userID, this.router.postID) ?? [];
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("Render: got a route I didn't understand. Rendering HOME:", this.router.route);
this.posts = await this.loadPosts(this.userID) ?? [];
this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
break;
}
}
let existingPostSet = new Set(existingPosts.map(post => post.post_id));
let incomingPostSet = new Set(this.posts.map(post => post.post_id));
let addedPosts = [];
for (let post of this.posts) {
if (!existingPostSet.has(post.post_id)) {
addedPosts.push(post);
}
}
let deletedPosts = [];
for (let post of existingPosts) {
if (!incomingPostSet.has(post.post_id)) {
deletedPosts.push(post);
}
}
console.log("added:", addedPosts, "removed:", deletedPosts);
const fragment = document.createDocumentFragment();
let contentDiv = document.getElementById("content");
if (!contentDiv) {
throw new Error();
}
if (this.posts.length === 0) {
contentDiv.innerHTML = `<div style="font-size:32px">Doing complicated shennanigans to load posts for you so just hang on a minute, ok!?</div>`;
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("added:", addedPosts, "removed:", deletedPosts);
const fragment = document.createDocumentFragment();
contentDiv.innerHTML = "";
// let count = 0;
let count = 0;
this.renderedPosts.clear();
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);
this.renderedPosts.set(postData.post_id, post);
if (post) {
fragment.appendChild(post);
// count++;
count++;
}
if (count > this.limitPosts) {
break;
}
// if (count > 100) {
// break;
// }
}
if (!contentDiv) {
throw new Error("Couldn't get content div!");
@@ -891,6 +1260,8 @@ class App {
contentDiv.appendChild(fragment);
let renderTime = this.timerDelta();
log(`render took: ${renderTime.toFixed(2)}ms`);
performance.mark("render-end");
performance.measure('render-time', 'render-start', 'render-end');
if (performance?.memory) {
log(`memory used: ${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`);
}
@@ -917,16 +1288,28 @@ class App {
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("<iframe")) {
// debugger;
// }
// markdown = this.replaceIframeWithDiv(markdown);
if (markdown.includes("<iframe") && markdown.includes(`src="https://dotbigbang`)) {
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>
<div>
<span class='header' title='${timestamp}'><img class="logo" src="/static/favicon.ico">@${post.author} -
<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>
</span>
${ownPost ? `<span id="deleteButton"></span>` : ''}
${ownPost ? `<span id="editButton"></span>` : ''}
<span id="shareButton"></span>
</div>
<div>${marked.parse(post.text)}</div>
<div>${markdown}</div>
</div>`;
containerDiv.innerHTML = postTemplate;
if (ownPost) {
@@ -959,7 +1342,6 @@ class App {
element.style.transform = "scale(2.0)";
}
getRoute() {
app.router.userID = this.userID;
let path = document.location.pathname;
console.log(">>>>>>>>>>>>>>>>>>>>>>>>>", path);
const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))";
@@ -984,7 +1366,7 @@ class App {
}
}
}
console.log(">>>>>>>>>>>>>", this.router, App.Route[this.router.route]);
console.log(">>>>>>>>>>>>>>>>>>>>>>>>>", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route]);
// user = /user/<ID>
// post = /user/<ID>/post/<ID>
// media = /user/<ID>/post/<ID>/media/<index>

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,7 @@ const contentToCache = [
'/static/main.js',
'/static/lib/marked.min.js',
'/static/lib/qrcode.min.js',
'/static/lib/d3.js',
'/static/db.js',
'/static/favicon.ico'
];

View File

@@ -1 +1 @@
{"version":3,"file":"sw.js","sourceRoot":"","sources":["../src/sw.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"sw.js","sourceRoot":"","sources":["../src/sw.ts"],"names":[],"mappings":"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"}