checkpoint

This commit is contained in:
bobbydigitales
2025-03-25 20:04:52 -07:00
parent 71b5284b0f
commit 0b5fee4aca
12 changed files with 240 additions and 138 deletions

View File

@@ -109,7 +109,7 @@ function helloHandler(m: HelloMessage, socket: WebSocket) {
userPeers.set(m.user_id, new Set()); userPeers.set(m.user_id, new Set());
} }
userPeers.get(m.user_id)?.add(m.peer_id); userPeers.get(m.user_id)?.add(m.peer_id);
peerSockets.set(m.peer_id, socket); peerSockets.set(m.peer_id, socket); // TODO:MAYBEBUG - what happens with multiple windows each with their own websocket?
socketPeers.set(socket, m.peer_id); socketPeers.set(socket, m.peer_id);
if (Symbol.iterator in Object(m.known_users)) { if (Symbol.iterator in Object(m.known_users)) {
@@ -123,9 +123,6 @@ function helloHandler(m: HelloMessage, socket: WebSocket) {
} }
} }
const returnValue: any = {}; const returnValue: any = {};
for (const key of userPeers.keys()) { for (const key of userPeers.keys()) {
const peers = userPeers.get(key); const peers = userPeers.get(key);
@@ -173,7 +170,10 @@ function peerMessageHandler(m: PeerMessage, _socket: WebSocket) {
const messageToSend = JSON.stringify(m); const messageToSend = JSON.stringify(m);
// console.log("ws->", toPeer, messageToSend); // console.log("ws->", toPeer, messageToSend);
console.log("peerMessageHandler: before toPeer.send");
toPeer.send(messageToSend) toPeer.send(messageToSend)
console.log("peerMessageHandler: after toPeer.send");
return null; return null;
} }
@@ -304,8 +304,8 @@ async function main() {
Deno.serve({ Deno.serve({
port: 6789, port: 6789,
cert: Deno.readTextFileSync("/etc/letsencrypt/live/ddlion.net/fullchain.pem"), cert: Deno.readTextFileSync("/etc/letsencrypt/live/ddln.app/fullchain.pem"),
key: Deno.readTextFileSync("/etc/letsencrypt/live/ddlion.net/privkey.pem"), key: Deno.readTextFileSync("/etc/letsencrypt/live/ddln.app/privkey.pem"),
}, handler); }, handler);
await devServerWatchFiles(); await devServerWatchFiles();

View File

@@ -10,8 +10,9 @@
// Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917 // Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917
const postStoreName: string = "posts"; const postStoreName: string = "posts";
const tombStoneStoreName: string = "tombstones" const tombstoneStoreName: string = "tombstones"
const followingStoreName: string = "following" const followingStoreName: string = "following"
const profileStoreName: string = "profiles"
let keyBase = "dandelion_posts_v1_" let keyBase = "dandelion_posts_v1_"
let key = ""; let key = "";
let version = 1; let version = 1;
@@ -33,32 +34,57 @@ async function upgrade_0to1(db: IDBDatabase) {
postsStore.createIndex("postIDIndex", "data.post_id", { unique: true }); postsStore.createIndex("postIDIndex", "data.post_id", { unique: true });
} }
// let following = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
// let profiles = [
// {
// id: 'b38b623c-c3fa-4351-9cab-50233c99fa4e',
// name: 'Robert Anderberg',
// handle: 'bobbydigitales',
// avatar_image: new ArrayBuffer(1024),
// banner_image: new ArrayBuffer(1024),
// description: "A very nice person who never does anything wrong.",
// }];
// let tombstones = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
async function upgrade_1to2(db: IDBDatabase) { async function upgrade_1to2(db: IDBDatabase) {
console.log("Upgrading database from 1 to 2"); let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
console.log("Converting all image arraybuffers to Blobs") let profileStore = db.createObjectStore(profileStoreName, { keyPath: "id", autoIncrement: true });
let tombstoneStore = db.createObjectStore(tombstoneStoreName, { keyPath: "id", autoIncrement: true });
tombstoneStore.createIndex("postIDIndex", "data.post_id", { unique: true });
// 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[]) { // async function upgrade_1to2(db: IDBDatabase) {
console.log(`Converting images for user ${userID}`) // console.log("Upgrading database from 1 to 2");
let posts = await getAllData(userID); // console.log("Converting all image arraybuffers to Blobs")
for (const post of posts) { // // TODO convert all images from arraybuffers to blobs
let image_data = post.data.image_data; // let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
if (image_data) { // if (knownUsers.length === 0) {
let blob = new Blob([image_data as ArrayBuffer]); // return;
post.data.image_data = blob; // }
// TODO get the format of a new post right // for (const userID of knownUsers as string[]) {
addData(userID, post); // 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) { async function upgrade_2to3(db: IDBDatabase) {
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true }); let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
@@ -89,7 +115,7 @@ export function openDatabase(userID: string): Promise<IDBDatabase> {
if (!upgradeFunction) { if (!upgradeFunction) {
throw new Error(`db: Don't have an upgrade function to go from version ${event.oldVersion} to version ${event.newVersion}`); throw new Error(`db: Don't have an upgrade function to go from version ${event.oldVersion} to version ${event.newVersion}`);
} }
debugger; // debugger;
await upgradeFunction(db); await upgradeFunction(db);
}; };

View File

@@ -542,7 +542,7 @@ class wsConnection {
knownUsers = knownUsers knownUsers = knownUsers
.filter(userID => this.shouldSyncUserID(userID)) .filter(userID => this.shouldSyncUserID(userID))
.filter(userID => !this.userBlockList.has(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. .filter(async userID => (await getAllIds(userID)).length > 0); // TODO:EASYOPT 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 ?? ""))); 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 }); 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 });
@@ -615,7 +615,7 @@ class wsConnection {
log("ws:connected"); log("ws:connected");
await this.sendHello(); await this.sendHello();
// If we're running as a headless peer, send a hello message every 60 seconds to refresh the posts we have. // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have.
let helloRefreshIntervalPeriod = 120; let helloRefreshIntervalPeriod = 120;
if (app.isHeadless) { if (app.isHeadless) {
console.log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod) console.log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod)
@@ -1200,34 +1200,55 @@ class App {
} }
button(elementName: string) {
return document.getElementById(elementName) as HTMLButtonElement;
}
div(elementName: string) {
return document.getElementById(elementName) as HTMLDivElement;
}
initButtons(userID: string, posts: StoragePost[], registration: ServiceWorkerRegistration | undefined) { initButtons(userID: string, posts: StoragePost[], registration: ServiceWorkerRegistration | undefined) {
// let font1Button = document.getElementById("button_font1") as HTMLButtonElement; // let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
// let font2Button = document.getElementById("button_font2") 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 toggleDark = document.getElementById('toggle_dark') as HTMLButtonElement;
// let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement; // let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement;
// let updateApp = document.getElementById("update_app") as HTMLButtonElement; // let updateApp = document.getElementById("update_app") as HTMLButtonElement;
// let ddlnLogoButton = document.getElementById('ddln_logo_button') as HTMLDivElement; // 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 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;
exportButton.addEventListener('click', async e => await this.exportPostsForUser(this.userID));
// toggleDark.addEventListener('click', () => { // toggleDark.addEventListener('click', () => {
// document.documentElement.style.setProperty('--main-bg-color', 'white'); // document.documentElement.style.setProperty('--main-bg-color', 'white');
// document.documentElement.style.setProperty('--main-fg-color', 'black'); // document.documentElement.style.setProperty('--main-fg-color', 'black');
// }) // })
let homeButton = this.div('home-button');
homeButton.addEventListener('click', e => window.location.href = `${window.location.origin}/`)
let profileButton = this.div('profile-button');
profileButton.addEventListener('click', e => window.location.href = `${window.location.origin}/user/${this.userID}`)
let monitorButton = this.div('monitor_button');
monitorButton.addEventListener('click', async () => {
navContainer.classList.toggle('active');
this.showInfo()
});
let navContainer = this.div('nav-container');
let burgerMenuButton = this.div('burger-menu-button');
burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
let exportButton = this.button("export-button");
exportButton.addEventListener('click', async e => await this.exportPostsForUser(this.userID));
let composeButton = this.div('compose-button');
composeButton.addEventListener('click', e => { composeButton.addEventListener('click', e => {
document.getElementById('compose')!.style.display = 'block'; document.getElementById('compose')!.style.display = 'block';
document.getElementById('textarea_post')?.focus(); document.getElementById('textarea_post')?.focus();
}); });
let filePicker = document.getElementById('file-input') as HTMLInputElement;
filePicker?.addEventListener('change', async (event: any) => { filePicker?.addEventListener('change', async (event: any) => {
for (let file of filePicker.files as any) { for (let file of filePicker.files as any) {
let buffer = await file.arrayBuffer(); let buffer = await file.arrayBuffer();
@@ -1238,6 +1259,7 @@ class App {
filePicker.value = ''; filePicker.value = '';
}); });
let filePickerLabel = document.getElementById('file-input-label');
filePickerLabel?.addEventListener('click', () => { filePickerLabel?.addEventListener('click', () => {
console.log("Add pic...") console.log("Add pic...")
}) })
@@ -1301,9 +1323,6 @@ class App {
// this.showInfo() // this.showInfo()
// }); // });
monitorButton.addEventListener('click', async () => {
this.showInfo()
});
} }
async getPostsForFeed() { async getPostsForFeed() {
@@ -1326,6 +1345,7 @@ class App {
async loadFollowersFromStorage(userID: string): Promise<string[]> { async loadFollowersFromStorage(userID: string): Promise<string[]> {
// Rob
if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') { if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') {
return [ return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e', 'b38b623c-c3fa-4351-9cab-50233c99fa4e',
@@ -1337,6 +1357,7 @@ class App {
] ]
} }
// Martin
if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') { if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') {
return [ return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e', 'b38b623c-c3fa-4351-9cab-50233c99fa4e',
@@ -1344,6 +1365,15 @@ class App {
] ]
} }
// Fiona
if (userID === '8f6802be-c3b6-46c1-969c-5f90cbe01479') {
return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e', // Rob
'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
'05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
]
}
return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :) return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :)
} }
@@ -1389,7 +1419,8 @@ class App {
// continue; // continue;
// } // }
console.log(`https://ddln.app/user/${userID}`); console.log(`${document.location.origin}/user/${userID}`);
// console.log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID); // console.log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID);
} }
@@ -1544,7 +1575,7 @@ class App {
this.username = this.getUsername(); this.username = this.getUsername();
await this.initDB(); await this.initDB();
this.connectURL = `https://${document.location.hostname}/connect/${this.userID}`; this.connectURL = `${document.location.origin}/connect/${this.userID}`;
document.getElementById('connectURL')!.innerHTML = `<a href="${this.connectURL}">connect</a>`; document.getElementById('connectURL')!.innerHTML = `<a href="${this.connectURL}">connect</a>`;
@@ -1802,7 +1833,7 @@ class App {
// let editButton = document.createElement('button'); editButton.innerText = 'edit'; // let editButton = document.createElement('button'); editButton.innerText = 'edit';
let shareButton = document.createElement('button'); shareButton.innerText = 'share'; let shareButton = document.createElement('button'); shareButton.innerText = 'share';
shareButton.onclick = async () => { shareButton.onclick = async () => {
let shareUrl = `https://${document.location.hostname}/user/${post.author_id}/post/${post.post_id}`; let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
await navigator.clipboard.writeText(shareUrl) await navigator.clipboard.writeText(shareUrl)
}; };
@@ -1824,7 +1855,7 @@ class App {
markdown = markdown.replace("<iframe", `<iframe style="width:100%;height:50px;display:none" onblur="this.style.display = 'inline';"`); 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 userURL = `${document.location.origin}/user/${post.author_id}/`
let postTemplate = let postTemplate =
`<div>${first ? '' : '<hr>'} `<div>${first ? '' : '<hr>'}

View File

@@ -7,8 +7,6 @@ class PeerManager {
} }
} }
class PeerConnection { class PeerConnection {
static config = { static config = {
iceServers: [ iceServers: [
@@ -17,13 +15,13 @@ class PeerConnection {
{ urls: "stun:stun2.l.google.com" }, { urls: "stun:stun2.l.google.com" },
{ urls: "stun:stun3.l.google.com" }, { urls: "stun:stun3.l.google.com" },
{ urls: "stun:stun4.l.google.com" }, { urls: "stun:stun4.l.google.com" },
],}; ],
};
} }
const config = { const config = {
iceServers: [{ urls: "stun:stun.mystunserver.tld" }], iceServers: [{ urls: "stun:stun.mystunserver.tld" }],
}; };

View File

@@ -3,11 +3,6 @@
"short_name": "ddln", "short_name": "ddln",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"display_override": [
"window-controls-overlay",
"standalone"
],
"id": "b1dbe643-36fc-4419-9448-80f32a1baa1a", "id": "b1dbe643-36fc-4419-9448-80f32a1baa1a",
"background_color": "#000000", "background_color": "#000000",
"theme_color": "#000000", "theme_color": "#000000",

View File

@@ -5,8 +5,9 @@
// } // }
// Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917 // Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917
const postStoreName = "posts"; const postStoreName = "posts";
const tombStoneStoreName = "tombstones"; const tombstoneStoreName = "tombstones";
const followingStoreName = "following"; const followingStoreName = "following";
const profileStoreName = "profiles";
let keyBase = "dandelion_posts_v1_"; let keyBase = "dandelion_posts_v1_";
let key = ""; let key = "";
let version = 1; let version = 1;
@@ -15,28 +16,45 @@ async function upgrade_0to1(db) {
postsStore.createIndex("datetimeIndex", "post_timestamp", { unique: false }); postsStore.createIndex("datetimeIndex", "post_timestamp", { unique: false });
postsStore.createIndex("postIDIndex", "data.post_id", { unique: true }); postsStore.createIndex("postIDIndex", "data.post_id", { unique: true });
} }
// let following = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
// let profiles = [
// {
// id: 'b38b623c-c3fa-4351-9cab-50233c99fa4e',
// name: 'Robert Anderberg',
// handle: 'bobbydigitales',
// avatar_image: new ArrayBuffer(1024),
// banner_image: new ArrayBuffer(1024),
// description: "A very nice person who never does anything wrong.",
// }];
// let tombstones = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
async function upgrade_1to2(db) { async function upgrade_1to2(db) {
console.log("Upgrading database from 1 to 2"); let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
console.log("Converting all image arraybuffers to Blobs"); let profileStore = db.createObjectStore(profileStoreName, { keyPath: "id", autoIncrement: true });
// TODO convert all images from arraybuffers to blobs let tombstoneStore = db.createObjectStore(tombstoneStoreName, { keyPath: "id", autoIncrement: true });
let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', '')); tombstoneStore.createIndex("postIDIndex", "data.post_id", { unique: true });
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_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) { async function upgrade_2to3(db) {
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true }); let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
} }
@@ -59,7 +77,7 @@ export function openDatabase(userID) {
if (!upgradeFunction) { if (!upgradeFunction) {
throw new Error(`db: Don't have an upgrade function to go from version ${event.oldVersion} to version ${event.newVersion}`); throw new Error(`db: Don't have an upgrade function to go from version ${event.oldVersion} to version ${event.newVersion}`);
} }
debugger; // debugger;
await upgradeFunction(db); await upgradeFunction(db);
}; };
request.onsuccess = (event) => { request.onsuccess = (event) => {

File diff suppressed because one or more lines are too long

View File

@@ -36,17 +36,17 @@
<body> <body>
<button class="burger-menu"></button> <div id="burger-menu-button" class="burger-menu"></div>
<div class="nav-overlay"></div> <div class="nav-overlay"></div>
<div class="flex-container"> <div class="flex-container">
<nav class="nav-container"> <nav id="nav-container" class="nav-container">
<a class="nav-item profile-pic"> <a class="nav-item profile-pic">
<span class="nav-profile" role="img" aria-label="Home"><img class="profile-pic-container" <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> src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:wobbjvmqx65vou5ipgpgs3wf/bafkreigbpq2unrhkxgisofimgqmiiiplp2ssmdgw6cqsa7i2ogtvlxqzfa@jpeg"></span>
</a> </a>
<a class="nav-item"> <a class="nav-item" id="home-button">
<span class="nav-emoji emoji-fill" role="img" aria-label="Home">🏠</span> <span class="nav-emoji emoji-fill" role="img" aria-label="Home">🏠</span>
<span class="nav-label">Home</span> <span class="nav-label">Home</span>
</a> </a>
@@ -58,14 +58,14 @@
<span class="nav-emoji" role="img" aria-label="Notifications">🔔</span> <span class="nav-emoji" role="img" aria-label="Notifications">🔔</span>
<span class="nav-label">Notifications</span> <span class="nav-label">Notifications</span>
</a> --> </a> -->
<a class="nav-item"> <a class="nav-item" id="profile-button">
<span class="nav-emoji emoji-fill" role="img" aria-label="Profile">👤</span> <span class="nav-emoji emoji-fill" role="img" aria-label="Profile">👤</span>
<span class="nav-label">Profile</span> <span class="nav-label">Profile</span>
</a> </a>
<a class="nav-item"> <!-- <a class="nav-item">
<span class="nav-emoji emoji-fill" role="img" aria-label="Settings">⚙️</span> <span class="nav-emoji emoji-fill" role="img" aria-label="Settings">⚙️</span>
<span class="nav-label">Settings</span> <span class="nav-label">Settings</span>
</a> </a> -->
<a id="monitor_button" class="nav-item"> <a id="monitor_button" class="nav-item">
<span class="nav-emoji" role="img" aria-label="Monitor">🤓</span> <span class="nav-emoji" role="img" aria-label="Monitor">🤓</span>
<span class="nav-label">Monitor</span> <span class="nav-label">Monitor</span>
@@ -80,6 +80,8 @@
<div id="status"></div> <div id="status"></div>
<div id="info" style="display:none"> <div id="info" style="display:none">
<button id="export-button">export</button>
<div id="profile"> <div id="profile">
<span class="form_label">username:</span><span class="form_field" id="username" <span class="form_label">username:</span><span class="form_field" id="username"
contenteditable="true">unnamed</span> contenteditable="true">unnamed</span>
@@ -108,7 +110,6 @@
<!-- <button id="button_font1" >font1</button> <!-- <button id="button_font1" >font1</button>
<button id="button_font2" >font2 </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="clear_posts">clear </button> -->
<!-- <button id="update_app">check for updates</button> --> <!-- <button id="update_app">check for updates</button> -->
<!-- <button id="toggle_dark">light/dark</button> --> <!-- <button id="toggle_dark">light/dark</button> -->
@@ -116,8 +117,8 @@
<textarea cols="60" rows="6" id="textarea_post"></textarea> <textarea cols="60" rows="6" id="textarea_post"></textarea>
<div class="right"> <div class="right">
<label for="file_input" id="file_input_label" class="button button-big">photo</label> <label for="file-input" id="file-input-label" class="button button-big">photo</label>
<input type="file" id="file_input" accept="image/*" 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_add_pic" >🏞️</button> -->
<button id="button_post" class="button button-big">post</button> <button id="button_post" class="button button-big">post</button>
@@ -126,7 +127,7 @@
<!-- <div id="torrent-content"></div> --> <!-- <div id="torrent-content"></div> -->
<div id="content"></div> <div id="content"></div>
<div id="compose_button" class="compose-button emoji-fill">✏️</div> <div id="compose-button" class="compose-button emoji-fill">✏️</div>
</div> </div>
</div> </div>

View File

@@ -221,10 +221,11 @@ iframe {
top: 0; top: 0;
height: 100vh; height: 100vh;
background-color: var(--main-bg-color); background-color: var(--main-bg-color);
border-right: 1px solid rgb(60, 60, 60); /* border-right: 1px solid rgb(60, 60, 60); */
/* transition: width 0.3s; */ /* transition: width 0.3s; */
overflow: hidden; overflow: hidden;
width: 150px; width: 130px;
margin-right: 5px;
} }
.nav-container.collapsed { .nav-container.collapsed {
@@ -259,10 +260,13 @@ iframe {
} }
.burger-menu { .burger-menu {
text-align: center;
width: 46px;
border-radius: 10px;
left: 8px;
display: none; display: none;
position: fixed; position: fixed;
left: 16px; top: 0px;
top: 16px;
font-size: 24px; font-size: 24px;
cursor: pointer; cursor: pointer;
z-index: 1000; z-index: 1000;
@@ -281,17 +285,22 @@ iframe {
@media (max-width: 700px) { @media (max-width: 700px) {
.nav-container { .nav-container {
transform: translateX(-100%); display: none;
z-index: 1000; /* transform: translateX(-100%); */
/* z-index: 1000; */
} }
.nav-container.active { .nav-container.active {
transform: translateX(0); position: fixed;
left: 0px;
display: block;
/* transform: translateX(0); */
width: 200px; width: 200px;
} }
.burger-menu { .burger-menu {
display: block; display: block;
background-color: var(--main-bg-color);
} }
.nav-overlay.active { .nav-overlay.active {
@@ -329,19 +338,19 @@ iframe {
} }
.compose-button { .compose-button {
font-size: 48px; font-size: 42px;
position: fixed; position: fixed;
bottom: 24px; bottom: 24px;
right: 24px; right: 24px;
width: 72px; width: 64px;
height: 72px; height: 64px;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
background: rgba(87, 87, 255, 0.8); background: rgba(87, 87, 255, 0.5);
} }
.compose-button:hover { .compose-button:hover {

View File

@@ -385,7 +385,7 @@ class wsConnection {
knownUsers = knownUsers knownUsers = knownUsers
.filter(userID => this.shouldSyncUserID(userID)) .filter(userID => this.shouldSyncUserID(userID))
.filter(userID => !this.userBlockList.has(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. .filter(async (userID) => (await getAllIds(userID)).length > 0); // TODO:EASYOPT 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 ?? ""))); 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 }); 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 });
} }
@@ -448,7 +448,7 @@ class wsConnection {
this.websocket.onopen = async (event) => { this.websocket.onopen = async (event) => {
log("ws:connected"); log("ws:connected");
await this.sendHello(); await this.sendHello();
// If we're running as a headless peer, send a hello message every 60 seconds to refresh the posts we have. // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have.
let helloRefreshIntervalPeriod = 120; let helloRefreshIntervalPeriod = 120;
if (app.isHeadless) { if (app.isHeadless) {
console.log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod); console.log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod);
@@ -906,29 +906,45 @@ class App {
document.querySelector('#qrcode > canvas').classList.add('qrcode_image'); document.querySelector('#qrcode > canvas').classList.add('qrcode_image');
this.showLog = true; this.showLog = true;
} }
button(elementName) {
return document.getElementById(elementName);
}
div(elementName) {
return document.getElementById(elementName);
}
initButtons(userID, posts, registration) { initButtons(userID, posts, registration) {
// let font1Button = document.getElementById("button_font1") as HTMLButtonElement; // let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
// let font2Button = document.getElementById("button_font2") 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"); // let toggleDark = document.getElementById('toggle_dark') as HTMLButtonElement;
// let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement; // let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement;
// let updateApp = document.getElementById("update_app") as HTMLButtonElement; // let updateApp = document.getElementById("update_app") as HTMLButtonElement;
// let ddlnLogoButton = document.getElementById('ddln_logo_button') as HTMLDivElement; // 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 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') as HTMLButtonElement;
exportButton.addEventListener('click', async (e) => await this.exportPostsForUser(this.userID));
// toggleDark.addEventListener('click', () => { // toggleDark.addEventListener('click', () => {
// document.documentElement.style.setProperty('--main-bg-color', 'white'); // document.documentElement.style.setProperty('--main-bg-color', 'white');
// document.documentElement.style.setProperty('--main-fg-color', 'black'); // document.documentElement.style.setProperty('--main-fg-color', 'black');
// }) // })
let homeButton = this.div('home-button');
homeButton.addEventListener('click', e => window.location.href = `${window.location.origin}/`);
let profileButton = this.div('profile-button');
profileButton.addEventListener('click', e => window.location.href = `${window.location.origin}/user/${this.userID}`);
let monitorButton = this.div('monitor_button');
monitorButton.addEventListener('click', async () => {
navContainer.classList.toggle('active');
this.showInfo();
});
let navContainer = this.div('nav-container');
let burgerMenuButton = this.div('burger-menu-button');
burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
let exportButton = this.button("export-button");
exportButton.addEventListener('click', async (e) => await this.exportPostsForUser(this.userID));
let composeButton = this.div('compose-button');
composeButton.addEventListener('click', e => { composeButton.addEventListener('click', e => {
document.getElementById('compose').style.display = 'block'; document.getElementById('compose').style.display = 'block';
document.getElementById('textarea_post')?.focus(); document.getElementById('textarea_post')?.focus();
}); });
let filePicker = document.getElementById('file-input');
filePicker?.addEventListener('change', async (event) => { filePicker?.addEventListener('change', async (event) => {
for (let file of filePicker.files) { for (let file of filePicker.files) {
let buffer = await file.arrayBuffer(); let buffer = await file.arrayBuffer();
@@ -937,6 +953,7 @@ class App {
// Reset so that if they pick the same image again, we still get the change event. // Reset so that if they pick the same image again, we still get the change event.
filePicker.value = ''; filePicker.value = '';
}); });
let filePickerLabel = document.getElementById('file-input-label');
filePickerLabel?.addEventListener('click', () => { filePickerLabel?.addEventListener('click', () => {
console.log("Add pic..."); console.log("Add pic...");
}); });
@@ -983,9 +1000,6 @@ class App {
// ddlnLogoButton.addEventListener('click', async () => { // ddlnLogoButton.addEventListener('click', async () => {
// this.showInfo() // this.showInfo()
// }); // });
monitorButton.addEventListener('click', async () => {
this.showInfo();
});
} }
async getPostsForFeed() { async getPostsForFeed() {
// get N posts from each user and sort them by date. // get N posts from each user and sort them by date.
@@ -1002,6 +1016,7 @@ class App {
return posts; return posts;
} }
async loadFollowersFromStorage(userID) { async loadFollowersFromStorage(userID) {
// Rob
if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') { if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') {
return [ return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e', 'b38b623c-c3fa-4351-9cab-50233c99fa4e',
@@ -1012,12 +1027,21 @@ class App {
'8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona '8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona
]; ];
} }
// Martin
if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') { if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') {
return [ return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e', 'b38b623c-c3fa-4351-9cab-50233c99fa4e',
'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
]; ];
} }
// Fiona
if (userID === '8f6802be-c3b6-46c1-969c-5f90cbe01479') {
return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e', // Rob
'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
'05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
];
}
return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :) return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :)
} }
async loadPostsFromStorage(userID, postID) { async loadPostsFromStorage(userID, postID) {
@@ -1052,7 +1076,7 @@ class App {
// indexedDB.deleteDatabase(`user_${userID}`); // indexedDB.deleteDatabase(`user_${userID}`);
// continue; // continue;
// } // }
console.log(`https://ddln.app/user/${userID}`); console.log(`${document.location.origin}/user/${userID}`);
// console.log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID); // console.log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID);
} }
} }
@@ -1165,7 +1189,7 @@ class App {
this.userID = this.getUserID(); this.userID = this.getUserID();
this.username = this.getUsername(); this.username = this.getUsername();
await this.initDB(); await this.initDB();
this.connectURL = `https://${document.location.hostname}/connect/${this.userID}`; this.connectURL = `${document.location.origin}/connect/${this.userID}`;
document.getElementById('connectURL').innerHTML = `<a href="${this.connectURL}">connect</a>`; document.getElementById('connectURL').innerHTML = `<a href="${this.connectURL}">connect</a>`;
let urlParams = (new URL(window.location.href)).searchParams; let urlParams = (new URL(window.location.href)).searchParams;
if (urlParams.has('log')) { if (urlParams.has('log')) {
@@ -1358,7 +1382,7 @@ class App {
let shareButton = document.createElement('button'); let shareButton = document.createElement('button');
shareButton.innerText = 'share'; shareButton.innerText = 'share';
shareButton.onclick = async () => { shareButton.onclick = async () => {
let shareUrl = `https://${document.location.hostname}/user/${post.author_id}/post/${post.post_id}`; let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
await navigator.clipboard.writeText(shareUrl); await navigator.clipboard.writeText(shareUrl);
}; };
let ownPost = post.author_id === this.userID; let ownPost = post.author_id === this.userID;
@@ -1373,7 +1397,7 @@ class App {
if (markdown.includes("<iframe") && markdown.includes(`src="https://dotbigbang`)) { 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';"`); 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 userURL = `${document.location.origin}/user/${post.author_id}/`;
let postTemplate = `<div>${first ? '' : '<hr>'} let postTemplate = `<div>${first ? '' : '<hr>'}
<div> <div>
<span class='header' title='${timestamp}'><img class="logo" src="/static/favicon.ico"><a class="username" href="${userURL}">@${post.author}</a> - <span class='header' title='${timestamp}'><img class="logo" src="/static/favicon.ico"><a class="username" href="${userURL}">@${post.author}</a> -

File diff suppressed because one or more lines are too long

View File

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