Compare commits
11 Commits
548ac39d19
...
bobbyd-exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8cc08e5cc | ||
| 4ae581b1a2 | |||
| e27cf391ef | |||
| 29ba02e3ce | |||
| 1353acc4d1 | |||
| 4dda9af788 | |||
| eb4dbb2448 | |||
| 3fef295b59 | |||
|
|
f22d8b9ba6 | ||
|
|
9c15ed2cd2 | ||
|
|
f387d9ea48 |
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": "deno run -A npm:typescript/bin/tsc",
|
"build": "deno run -A npm:typescript/bin/tsc",
|
||||||
"watch": "deno run -A npm:typescript/bin/tsc --watch"
|
"watch": "deno run -A npm:typescript/bin/tsc --watch",
|
||||||
|
"test": "deno test --allow-net src/App.test.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
deno.lock
generated
31
deno.lock
generated
@@ -3,14 +3,17 @@
|
|||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@deno-library/compress@*": "0.5.6",
|
"jsr:@deno-library/compress@*": "0.5.6",
|
||||||
"jsr:@deno-library/crc32@1.0.2": "1.0.2",
|
"jsr:@deno-library/crc32@1.0.2": "1.0.2",
|
||||||
|
"jsr:@std/assert@*": "1.0.19",
|
||||||
"jsr:@std/bytes@^1.0.2": "1.0.6",
|
"jsr:@std/bytes@^1.0.2": "1.0.6",
|
||||||
"jsr:@std/fs@1.0.5": "1.0.5",
|
"jsr:@std/fs@1.0.5": "1.0.5",
|
||||||
|
"jsr:@std/internal@^1.0.12": "1.0.12",
|
||||||
"jsr:@std/io@0.225.0": "0.225.0",
|
"jsr:@std/io@0.225.0": "0.225.0",
|
||||||
"jsr:@std/path@1.0.8": "1.0.8",
|
"jsr:@std/path@1.0.8": "1.0.8",
|
||||||
"jsr:@std/path@^1.0.7": "1.0.8",
|
"jsr:@std/path@^1.0.7": "1.0.8",
|
||||||
"jsr:@std/streams@^1.0.7": "1.0.17",
|
"jsr:@std/streams@^1.0.7": "1.0.17",
|
||||||
"jsr:@std/tar@0.1.3": "0.1.3",
|
"jsr:@std/tar@0.1.3": "0.1.3",
|
||||||
"jsr:@zip-js/zip-js@2.7.53": "2.7.53",
|
"jsr:@zip-js/zip-js@2.7.53": "2.7.53",
|
||||||
|
"npm:playwright@*": "1.59.1",
|
||||||
"npm:typescript@*": "6.0.2"
|
"npm:typescript@*": "6.0.2"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
@@ -28,6 +31,12 @@
|
|||||||
"@deno-library/crc32@1.0.2": {
|
"@deno-library/crc32@1.0.2": {
|
||||||
"integrity": "d2061bfee30c87c97f285dfca0fdc4458e632dc072a33ecfc73ca5177a5a39a0"
|
"integrity": "d2061bfee30c87c97f285dfca0fdc4458e632dc072a33ecfc73ca5177a5a39a0"
|
||||||
},
|
},
|
||||||
|
"@std/assert@1.0.19": {
|
||||||
|
"integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@std/bytes@1.0.6": {
|
"@std/bytes@1.0.6": {
|
||||||
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
|
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
|
||||||
},
|
},
|
||||||
@@ -37,6 +46,9 @@
|
|||||||
"jsr:@std/path@^1.0.7"
|
"jsr:@std/path@^1.0.7"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@std/internal@1.0.12": {
|
||||||
|
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
|
||||||
|
},
|
||||||
"@std/io@0.225.0": {
|
"@std/io@0.225.0": {
|
||||||
"integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3",
|
"integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -60,6 +72,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
|
"fsevents@2.3.2": {
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"scripts": true
|
||||||
|
},
|
||||||
|
"playwright-core@1.59.1": {
|
||||||
|
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"playwright@1.59.1": {
|
||||||
|
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||||
|
"dependencies": [
|
||||||
|
"playwright-core"
|
||||||
|
],
|
||||||
|
"optionalDependencies": [
|
||||||
|
"fsevents"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
"typescript@6.0.2": {
|
"typescript@6.0.2": {
|
||||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||||
"bin": true
|
"bin": true
|
||||||
|
|||||||
@@ -292,12 +292,14 @@ function connectWebsocket(request: Request) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectRoot = new URL('..', import.meta.url).pathname;
|
||||||
|
|
||||||
async function devServerWatchFiles() {
|
async function devServerWatchFiles() {
|
||||||
const watcher = Deno.watchFs(["../static/", "../src/"]);
|
const watcher = Deno.watchFs(["../static/", "../src/"]);
|
||||||
for await (const event of watcher) {
|
for await (const event of watcher) {
|
||||||
if (event.kind === "modify") {
|
if (event.kind === "modify") {
|
||||||
for (const path of event.paths) {
|
for (const path of event.paths) {
|
||||||
const cachedPath = path.replace(Deno.cwd() + '/..', '')
|
const cachedPath = path.replace(projectRoot, '/');
|
||||||
filepathResponseCache.delete(cachedPath);
|
filepathResponseCache.delete(cachedPath);
|
||||||
console.log('Purging updated file:', cachedPath)
|
console.log('Purging updated file:', cachedPath)
|
||||||
}
|
}
|
||||||
|
|||||||
9
setup.sh
9
setup.sh
@@ -9,15 +9,6 @@ if ! command -v deno &>/dev/null; then
|
|||||||
export PATH="$DENO_INSTALL/bin:$PATH"
|
export PATH="$DENO_INSTALL/bin:$PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install node if needed
|
|
||||||
if ! command -v node &>/dev/null; then
|
|
||||||
echo "Installing node..."
|
|
||||||
brew install node
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install TypeScript dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Install tmux if needed
|
# Install tmux if needed
|
||||||
if ! command -v tmux &>/dev/null; then
|
if ! command -v tmux &>/dev/null; then
|
||||||
echo "Installing tmux..."
|
echo "Installing tmux..."
|
||||||
|
|||||||
338
src/App.ts
338
src/App.ts
@@ -1,8 +1,8 @@
|
|||||||
import { generateID } from "IDUtils";
|
import { generateID } from "IDUtils";
|
||||||
import { PeerManager, PeerEventTypes } from "PeerManager";
|
import { PeerManager, PeerEventTypes } from "PeerManager";
|
||||||
import { Sync } from "Sync";
|
import { Sync } from "Sync";
|
||||||
import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds, getPostForUser } from "db";
|
import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds, getPostForUser, getPostById, getRepliesForPost, buildReplyCountMap, getNotificationsForUser } from "db";
|
||||||
import { arrayBufferToBase64, compressString } from "dataUtils";
|
import { arrayBufferToBase64, compressString, decompressBuffer, base64ToArrayBuffer } from "dataUtils";
|
||||||
import { log, logID, renderLog, setLogVisibility } from "log"
|
import { log, logID, renderLog, setLogVisibility } from "log"
|
||||||
|
|
||||||
declare let marked: any;
|
declare let marked: any;
|
||||||
@@ -14,6 +14,7 @@ class Post {
|
|||||||
post_timestamp: Date;
|
post_timestamp: Date;
|
||||||
post_id: string;
|
post_id: string;
|
||||||
reply_to_id: string|null;
|
reply_to_id: string|null;
|
||||||
|
root_id: string|null;
|
||||||
author: string;
|
author: string;
|
||||||
author_id: string;
|
author_id: string;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -29,7 +30,8 @@ class Post {
|
|||||||
imageData: ArrayBuffer | null = null,
|
imageData: ArrayBuffer | null = null,
|
||||||
importedFrom: "twitter" | null = null,
|
importedFrom: "twitter" | null = null,
|
||||||
importSource: any = null,
|
importSource: any = null,
|
||||||
reply_to_id:string|null = null) {
|
reply_to_id: string|null = null,
|
||||||
|
root_id: string|null = null) {
|
||||||
|
|
||||||
this.post_timestamp = post_timestamp;
|
this.post_timestamp = post_timestamp;
|
||||||
this.post_id = generateID();
|
this.post_id = generateID();
|
||||||
@@ -42,6 +44,7 @@ class Post {
|
|||||||
this.importedFrom = importedFrom;
|
this.importedFrom = importedFrom;
|
||||||
this.importSource = importSource;
|
this.importSource = importSource;
|
||||||
this.reply_to_id = reply_to_id;
|
this.reply_to_id = reply_to_id;
|
||||||
|
this.root_id = root_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +123,7 @@ export class App {
|
|||||||
userID: string = '';
|
userID: string = '';
|
||||||
peerID: string = '';
|
peerID: string = '';
|
||||||
replyToID: string|null = null;
|
replyToID: string|null = null;
|
||||||
|
replyRootID: string|null = null;
|
||||||
following: Set<string> = new Set();
|
following: Set<string> = new Set();
|
||||||
posts: StoragePost[] = [];
|
posts: StoragePost[] = [];
|
||||||
isHeadless: boolean = false;
|
isHeadless: boolean = false;
|
||||||
@@ -524,17 +528,74 @@ export class App {
|
|||||||
globalThis.URL.revokeObjectURL(url);
|
globalThis.URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
async importPostsForUser(userID: string, posts: string) {
|
async importPostsForUser(buffer: ArrayBuffer) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
console.log.apply(null, log("Importing posts"));
|
||||||
|
const json = await decompressBuffer(buffer);
|
||||||
|
const data = JSON.parse(json);
|
||||||
|
|
||||||
|
let postsByUser: { [userID: string]: any[] };
|
||||||
|
let username = this.username;
|
||||||
|
let userID = this.userID;
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
console.log.apply(null, log("Detected old export format"));
|
||||||
|
postsByUser = { [this.userID]: data };
|
||||||
|
} else {
|
||||||
|
console.log.apply(null, log("Detected new export format"));
|
||||||
|
const { username: importedUsername, userID: importedUserID, posts } = data;
|
||||||
|
username = importedUsername;
|
||||||
|
userID = importedUserID;
|
||||||
|
postsByUser = posts;
|
||||||
|
localStorage.setItem("dandelion_username", username);
|
||||||
|
localStorage.setItem("dandelion_id", userID);
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportPostsForUser(userID: string) {
|
let totalPostsImported = 0;
|
||||||
|
const userTimings: { [userID: string]: number } = {};
|
||||||
|
|
||||||
let posts = await getAllData(userID);
|
for (const [sourceUserID, posts] of Object.entries(postsByUser)) {
|
||||||
|
const userStartTime = performance.now();
|
||||||
|
const postList = posts as any[];
|
||||||
|
|
||||||
let output = [];
|
for (let post of postList) {
|
||||||
|
if (post.image_data && typeof post.image_data === 'string') {
|
||||||
|
post.image_data = await base64ToArrayBuffer(post.image_data);
|
||||||
|
}
|
||||||
|
if (post.post_timestamp && typeof post.post_timestamp === 'string') {
|
||||||
|
post.post_timestamp = new Date(post.post_timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mergeDataArray(sourceUserID, postList);
|
||||||
|
totalPostsImported += postList.length;
|
||||||
|
userTimings[sourceUserID] = performance.now() - userStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const userTimingsLog = Object.entries(userTimings)
|
||||||
|
.map(([uid, ms]) => `${logID(uid)}: ${ms.toFixed(2)}ms`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
console.log.apply(null, log(`Imported ${totalPostsImported} posts from ${Object.keys(postsByUser).length} users in ${totalTime.toFixed(2)}ms (${userTimingsLog})`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportPostsForUser() {
|
||||||
|
console.log.apply(null, log("Exporting all posts for all users"));
|
||||||
|
const exportStartTime = performance.now();
|
||||||
|
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', ''))
|
||||||
|
.filter((userID): userID is string => userID !== undefined);
|
||||||
|
|
||||||
|
const postsByUser: { [userID: string]: any[] } = {};
|
||||||
|
const userTimings: { [userID: string]: number } = {};
|
||||||
|
|
||||||
|
for (const userID of knownUsers) {
|
||||||
|
const userStartTime = performance.now();
|
||||||
|
const posts = await getAllData(userID);
|
||||||
|
const output = [];
|
||||||
|
|
||||||
console.log.apply(null, log("Serializing images"));
|
|
||||||
for (let post of posts) {
|
for (let post of posts) {
|
||||||
let newPost = (post as any).data;
|
let newPost = (post as any).data;
|
||||||
|
|
||||||
@@ -545,7 +606,27 @@ export class App {
|
|||||||
output.push(newPost);
|
output.push(newPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
let compressedData = await compressString(JSON.stringify(output));
|
if (output.length > 0) {
|
||||||
|
postsByUser[userID] = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
userTimings[userID] = performance.now() - userStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - exportStartTime;
|
||||||
|
const userTimingsLog = Object.entries(userTimings)
|
||||||
|
.map(([uid, ms]) => `${logID(uid)}: ${ms.toFixed(2)}ms`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
console.log.apply(null, log(`Exported ${Object.keys(postsByUser).length} users in ${totalTime.toFixed(2)}ms (${userTimingsLog})`));
|
||||||
|
|
||||||
|
const exportData = {
|
||||||
|
username: this.username,
|
||||||
|
userID: this.userID,
|
||||||
|
posts: postsByUser
|
||||||
|
};
|
||||||
|
|
||||||
|
let compressedData = await compressString(JSON.stringify(exportData));
|
||||||
|
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
const timestamp = `${d.getFullYear()
|
const timestamp = `${d.getFullYear()
|
||||||
@@ -555,8 +636,7 @@ export class App {
|
|||||||
}_${String(d.getMinutes()).padStart(2, '0')
|
}_${String(d.getMinutes()).padStart(2, '0')
|
||||||
}_${String(d.getSeconds()).padStart(2, '0')}`;
|
}_${String(d.getSeconds()).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
this.downloadBinary(compressedData, `ddln_export_${timestamp}.json.gz`);
|
||||||
this.downloadBinary(compressedData, `ddln_${this.username}_export_${timestamp}.json.gz`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async importTweetArchive(userID: string, tweetArchive: any[]) {
|
async importTweetArchive(userID: string, tweetArchive: any[]) {
|
||||||
@@ -723,7 +803,7 @@ export class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/svg+xml" | "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4", replyToID:string|null = null) {
|
async createPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/svg+xml" | "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4", replyToID: string|null = null, replyRootID: string|null = null) {
|
||||||
if ((typeof postText !== "string") || postText.length === 0) {
|
if ((typeof postText !== "string") || postText.length === 0) {
|
||||||
console.log.apply(null, log("Not posting an empty string..."));
|
console.log.apply(null, log("Not posting an empty string..."));
|
||||||
return;
|
return;
|
||||||
@@ -738,7 +818,7 @@ export class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID);
|
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID, replyRootID);
|
||||||
// this.posts.push(post);
|
// this.posts.push(post);
|
||||||
// localStorage.setItem(key, JSON.stringify(posts));
|
// localStorage.setItem(key, JSON.stringify(posts));
|
||||||
addData(userID, post);
|
addData(userID, post);
|
||||||
@@ -916,6 +996,15 @@ export class App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideInfo() {
|
||||||
|
const infoElement = document.getElementById('info');
|
||||||
|
if (infoElement && infoElement.style.display !== 'none') {
|
||||||
|
infoElement.style.display = 'none';
|
||||||
|
setLogVisibility(false);
|
||||||
|
this.showLog = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showInfo() {
|
showInfo() {
|
||||||
let infoElement = document.getElementById('info');
|
let infoElement = document.getElementById('info');
|
||||||
|
|
||||||
@@ -962,6 +1051,15 @@ export class App {
|
|||||||
let profileButton = this.div('profile-button');
|
let profileButton = this.div('profile-button');
|
||||||
profileButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/user/${this.userID}`)
|
profileButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/user/${this.userID}`)
|
||||||
|
|
||||||
|
let notificationsButton = this.div('notifications-button');
|
||||||
|
notificationsButton.addEventListener('click', () => {
|
||||||
|
this.hideInfo();
|
||||||
|
navContainer.classList.remove('active');
|
||||||
|
history.pushState({}, '', '/notifications');
|
||||||
|
this.getRoute();
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
let monitorButton = this.div('monitor_button');
|
let monitorButton = this.div('monitor_button');
|
||||||
monitorButton.addEventListener('click', async () => {
|
monitorButton.addEventListener('click', async () => {
|
||||||
navContainer.classList.toggle('active');
|
navContainer.classList.toggle('active');
|
||||||
@@ -972,10 +1070,21 @@ export class App {
|
|||||||
let burgerMenuButton = this.div('burger-menu-button');
|
let burgerMenuButton = this.div('burger-menu-button');
|
||||||
burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
|
burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
|
||||||
|
|
||||||
|
let importFilePicker = document.getElementById('import-file-input') as HTMLInputElement;
|
||||||
|
importFilePicker?.addEventListener('change', async () => {
|
||||||
|
const file = importFilePicker.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
await this.importPostsForUser(buffer);
|
||||||
|
importFilePicker.value = '';
|
||||||
|
this.userID = localStorage.getItem("dandelion_id") || this.userID;
|
||||||
|
this.username = localStorage.getItem("dandelion_username") || this.username;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
let exportButton = this.button("export-button");
|
let exportButton = this.button("export-button");
|
||||||
exportButton.addEventListener('click', async e => {
|
exportButton.addEventListener('click', async e => {
|
||||||
|
await this.exportPostsForUser()
|
||||||
await this.exportPostsForUser(this.userID)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let composeButton = this.div('compose-button');
|
let composeButton = this.div('compose-button');
|
||||||
@@ -988,7 +1097,7 @@ export class App {
|
|||||||
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();
|
||||||
await this.createPost(this.userID, 'image...', buffer, file.type);
|
await this.createPost(this.userID, 'image...', buffer, file.type, this.replyToID, this.replyRootID);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -1047,12 +1156,21 @@ export class App {
|
|||||||
const dataTransfer = e.clipboardData
|
const dataTransfer = e.clipboardData
|
||||||
const file = dataTransfer!.files[0];
|
const file = dataTransfer!.files[0];
|
||||||
let buffer = await file.arrayBuffer();
|
let buffer = await file.arrayBuffer();
|
||||||
await this.createPost(this.userID, 'image...', buffer, file.type as any);
|
await this.createPost(this.userID, 'image...', buffer, file.type as any, this.replyToID, this.replyRootID);
|
||||||
});
|
});
|
||||||
|
|
||||||
postButton.addEventListener("click", () => {
|
const submitPost = () => {
|
||||||
this.createPost(userID, postText.value, undefined, undefined, this.replyToID);
|
this.createPost(userID, postText.value, undefined, undefined, this.replyToID, this.replyRootID);
|
||||||
this.exitCompose();
|
this.exitCompose();
|
||||||
|
};
|
||||||
|
|
||||||
|
postButton.addEventListener("click", submitPost);
|
||||||
|
|
||||||
|
postText.addEventListener("keydown", (e) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
submitPost();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// updateApp.addEventListener("click", () => {
|
// updateApp.addEventListener("click", () => {
|
||||||
@@ -1068,21 +1186,24 @@ export class App {
|
|||||||
|
|
||||||
|
|
||||||
// Change this all to a template so we're not toggling state in this crazy way!
|
// Change this all to a template so we're not toggling state in this crazy way!
|
||||||
enterCompose(replyToID:string|null=null) {
|
enterCompose(replyToID: string|null = null, replyRootID: string|null = null) {
|
||||||
|
this.replyToID = replyToID;
|
||||||
|
this.replyRootID = replyRootID;
|
||||||
|
|
||||||
if (replyToID) {
|
if (replyToID) {
|
||||||
this.renderComposeReplyArea(replyToID);
|
this.renderComposeReplyArea(replyToID);
|
||||||
document.getElementById("compose-reply-area")!.style.display = "block";
|
document.getElementById("compose-reply-area")!.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
replyToID = replyToID;
|
document.getElementById('compose')!.style.display = 'flex';
|
||||||
|
|
||||||
document.getElementById('compose')!.style.display = 'block';
|
|
||||||
document.getElementById('textarea_post')?.focus();
|
document.getElementById('textarea_post')?.focus();
|
||||||
document.getElementById('compose-dimmer')?.classList.add("compose-dimmer-dimmed");
|
document.getElementById('compose-dimmer')?.classList.add("compose-dimmer-dimmed");
|
||||||
document.body.classList.add("no-scroll");
|
document.body.classList.add("no-scroll");
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCompose() {
|
exitCompose() {
|
||||||
|
this.replyToID = null;
|
||||||
|
this.replyRootID = null;
|
||||||
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
|
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
|
||||||
postText.value = "";
|
postText.value = "";
|
||||||
document.getElementById('compose')!.style.display = 'none';
|
document.getElementById('compose')!.style.display = 'none';
|
||||||
@@ -1276,6 +1397,8 @@ export class App {
|
|||||||
|
|
||||||
await this.initDB();
|
await this.initDB();
|
||||||
|
|
||||||
|
window.addEventListener('popstate', () => { this.hideInfo(); this.getRoute(); this.render(); });
|
||||||
|
|
||||||
this.connectURL = `${document.location.origin}/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>`;
|
||||||
|
|
||||||
@@ -1420,6 +1543,11 @@ export class App {
|
|||||||
compose.style.display = "none";
|
compose.style.display = "none";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case App.Route.NOTIFICATIONS: {
|
||||||
|
this.posts = await getNotificationsForUser(this.userID);
|
||||||
|
document.getElementById('compose')!.style.display = "none";
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
console.log.apply(null, log("Render: got a route I didn't understand. Rendering HOME:", this.router.route));
|
console.log.apply(null, log("Render: got a route I didn't understand. Rendering HOME:", this.router.route));
|
||||||
this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
|
this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
|
||||||
@@ -1430,6 +1558,21 @@ export class App {
|
|||||||
if (!contentDiv) {
|
if (!contentDiv) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.router.route === App.Route.NOTIFICATIONS) {
|
||||||
|
contentDiv.innerHTML = '';
|
||||||
|
if (this.posts.length === 0) {
|
||||||
|
contentDiv.innerHTML = '<div style="padding:20px;color:gray">No notifications yet.</div>';
|
||||||
|
} else {
|
||||||
|
let first = true;
|
||||||
|
for (const postRecord of this.posts) {
|
||||||
|
contentDiv.appendChild(this.renderNotificationItem(postRecord.data, first));
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.posts.length === 0) {
|
if (this.posts.length === 0) {
|
||||||
this.renderWelcome(contentDiv as HTMLDivElement);
|
this.renderWelcome(contentDiv as HTMLDivElement);
|
||||||
return;
|
return;
|
||||||
@@ -1460,13 +1603,19 @@ export class App {
|
|||||||
contentDiv.innerHTML = "";
|
contentDiv.innerHTML = "";
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
const isPostView = this.router.route === App.Route.POST;
|
||||||
|
contentDiv.classList.toggle('post-view', isPostView);
|
||||||
|
|
||||||
|
const replyCountMap = await buildReplyCountMap();
|
||||||
|
|
||||||
this.renderedPosts.clear();
|
this.renderedPosts.clear();
|
||||||
let first = true;
|
let first = true;
|
||||||
for (let i = this.posts.length - 1; i >= 0; i--) {
|
for (let i = this.posts.length - 1; i >= 0; i--) {
|
||||||
let postData = this.posts[i];
|
let postData = this.posts[i];
|
||||||
|
if (!isPostView && postData.data.reply_to_id) continue;
|
||||||
// this.postsSet.add(postData);
|
// this.postsSet.add(postData);
|
||||||
// TODO return promises for all image loads and await those.
|
// TODO return promises for all image loads and await those.
|
||||||
let post = this.renderPost(postData.data, first);
|
let post = this.renderPost(postData.data, first, replyCountMap.recursive.get(postData.data.post_id) ?? 0);
|
||||||
first = false;
|
first = false;
|
||||||
// this.renderedPosts.set(postData.post_id, post);
|
// this.renderedPosts.set(postData.post_id, post);
|
||||||
if (post) {
|
if (post) {
|
||||||
@@ -1485,6 +1634,39 @@ export class App {
|
|||||||
|
|
||||||
contentDiv.appendChild(fragment);
|
contentDiv.appendChild(fragment);
|
||||||
|
|
||||||
|
if (isPostView && this.posts.length > 0) {
|
||||||
|
const currentPost = this.posts[0].data;
|
||||||
|
if (currentPost.root_id) {
|
||||||
|
const rootRecord = await getPostById(currentPost.root_id);
|
||||||
|
if (rootRecord) {
|
||||||
|
const rootEl = this.renderPost(rootRecord.data, true, replyCountMap.recursive.get(rootRecord.data.post_id) ?? 0);
|
||||||
|
contentDiv.insertBefore(rootEl, contentDiv.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderReplies = async (postID: string, depth: number) => {
|
||||||
|
if (depth > 2) return;
|
||||||
|
const replies = await getRepliesForPost(postID);
|
||||||
|
// Top-level replies: newest first. Nested replies: oldest first (readable order).
|
||||||
|
if (depth === 0) {
|
||||||
|
replies.sort((a, b) =>
|
||||||
|
new Date(b.data.post_timestamp).getTime() - new Date(a.data.post_timestamp).getTime()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
replies.sort((a, b) =>
|
||||||
|
new Date(a.data.post_timestamp).getTime() - new Date(b.data.post_timestamp).getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const reply of replies) {
|
||||||
|
const el = this.renderPost(reply.data, false, replyCountMap.direct.get(reply.data.post_id) ?? 0);
|
||||||
|
if (depth > 0) el.style.marginLeft = `${depth * 20}px`;
|
||||||
|
contentDiv.appendChild(el);
|
||||||
|
await renderReplies(reply.data.post_id, depth + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await renderReplies(this.posts[0].data.post_id, currentPost.root_id ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
let renderTime = this.timerDelta();
|
let renderTime = this.timerDelta();
|
||||||
|
|
||||||
console.log.apply(null, log(`render took: ${renderTime.toFixed(2)}ms`));;
|
console.log.apply(null, log(`render took: ${renderTime.toFixed(2)}ms`));;
|
||||||
@@ -1505,13 +1687,75 @@ export class App {
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderComposeReplyArea(replyToID:string) {
|
async renderComposeReplyArea(replyToID: string) {
|
||||||
let composeReplyArea = document.getElementById('compose-reply-area') as HTMLElement;
|
const composeReplyArea = document.getElementById('compose-reply-area') as HTMLElement;
|
||||||
composeReplyArea.innerText = replyToID;
|
composeReplyArea.innerHTML = '';
|
||||||
composeReplyArea.classList.add("show");
|
|
||||||
|
const record = await getPostById(replyToID);
|
||||||
|
if (!record) return;
|
||||||
|
const postData = record.data;
|
||||||
|
|
||||||
|
const authorDiv = document.createElement('div');
|
||||||
|
authorDiv.className = 'compose-reply-author';
|
||||||
|
authorDiv.textContent = `@${postData.author}`;
|
||||||
|
composeReplyArea.appendChild(authorDiv);
|
||||||
|
|
||||||
|
const previewDiv = document.createElement('div');
|
||||||
|
previewDiv.className = 'compose-reply-preview';
|
||||||
|
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
textDiv.className = 'compose-reply-preview-text';
|
||||||
|
textDiv.innerHTML = this.markedAvailable ? marked.parse(postData.text) : postData.text;
|
||||||
|
previewDiv.appendChild(textDiv);
|
||||||
|
|
||||||
|
if (postData.image_data) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.className = 'compose-reply-preview-image';
|
||||||
|
const blob = new Blob([postData.image_data as ArrayBuffer]);
|
||||||
|
img.src = URL.createObjectURL(blob);
|
||||||
|
previewDiv.appendChild(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPost(post: Post, first: boolean) {
|
composeReplyArea.appendChild(previewDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNotificationItem(post: Post, first: boolean): HTMLElement {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.style.cssText = 'padding: 8px 10px; cursor: pointer;';
|
||||||
|
if (!first) el.innerHTML = '<hr>';
|
||||||
|
|
||||||
|
const authorDiv = document.createElement('div');
|
||||||
|
authorDiv.className = 'compose-reply-author';
|
||||||
|
authorDiv.textContent = `@${post.author} · ${post.post_timestamp.toLocaleDateString()}`;
|
||||||
|
el.appendChild(authorDiv);
|
||||||
|
|
||||||
|
const previewDiv = document.createElement('div');
|
||||||
|
previewDiv.className = 'compose-reply-preview';
|
||||||
|
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
textDiv.className = 'compose-reply-preview-text';
|
||||||
|
textDiv.style.maxHeight = 'none';
|
||||||
|
textDiv.innerHTML = this.markedAvailable ? marked.parse(post.text) : post.text;
|
||||||
|
previewDiv.appendChild(textDiv);
|
||||||
|
|
||||||
|
if (post.image_data) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.className = 'compose-reply-preview-image';
|
||||||
|
img.src = URL.createObjectURL(new Blob([post.image_data as ArrayBuffer]));
|
||||||
|
previewDiv.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.appendChild(previewDiv);
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
history.pushState({}, '', `/user/${post.author_id}/post/${post.post_id}`);
|
||||||
|
this.getRoute();
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPost(post: Post, first: boolean, replyCount: number = 0) {
|
||||||
if (!(post.hasOwnProperty("text"))) {
|
if (!(post.hasOwnProperty("text"))) {
|
||||||
throw new Error("Post is malformed!");
|
throw new Error("Post is malformed!");
|
||||||
}
|
}
|
||||||
@@ -1530,13 +1774,11 @@ export class App {
|
|||||||
await navigator.clipboard.writeText(shareUrl)
|
await navigator.clipboard.writeText(shareUrl)
|
||||||
};
|
};
|
||||||
|
|
||||||
let replyButton = document.createElement('button'); replyButton.innerText = 'reply';
|
let replyButton = document.createElement('button'); replyButton.innerText = replyCount > 0 ? `reply (${replyCount})` : 'reply';
|
||||||
replyButton.onclick = async () => {
|
replyButton.onclick = async () => {
|
||||||
console.log(`replying to post ${post.post_id}`);
|
console.log(`replying to post ${post.post_id}`);
|
||||||
this.enterCompose(post.post_id);
|
const rootID = post.root_id ?? post.post_id;
|
||||||
// let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
|
this.enterCompose(post.post_id, rootID);
|
||||||
|
|
||||||
// await navigator.clipboard.writeText(shareUrl)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1560,7 +1802,8 @@ export class App {
|
|||||||
let userURL = `${document.location.origin}/user/${post.author_id}/`
|
let userURL = `${document.location.origin}/user/${post.author_id}/`
|
||||||
|
|
||||||
let postTemplate =
|
let postTemplate =
|
||||||
`<div>${first ? '' : '<hr>'}
|
`<div class="post-container">${first ? '' : '<hr>'}
|
||||||
|
<div class="post-body">
|
||||||
<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> -
|
||||||
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
|
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
|
||||||
@@ -1574,12 +1817,18 @@ export class App {
|
|||||||
${ownPost ? `<span id="editButton"></span>` : ''}
|
${ownPost ? `<span id="editButton"></span>` : ''}
|
||||||
<span id="shareButton"></span>
|
<span id="shareButton"></span>
|
||||||
${ownPost ? `<span id="deleteButton"></span>` : ''}
|
${ownPost ? `<span id="deleteButton"></span>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
containerDiv.innerHTML = postTemplate;
|
containerDiv.innerHTML = postTemplate;
|
||||||
|
|
||||||
|
const postBody = containerDiv.querySelector('.post-body') as HTMLElement;
|
||||||
|
postBody.addEventListener('click', (e) => {
|
||||||
|
if ((e.target as HTMLElement).closest('button, a')) return;
|
||||||
|
history.pushState({}, '', `/user/${post.author_id}/post/${post.post_id}`);
|
||||||
|
this.getRoute();
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
if (ownPost) {
|
if (ownPost) {
|
||||||
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
|
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
|
||||||
@@ -1631,6 +1880,15 @@ export class App {
|
|||||||
let path = document.location.pathname;
|
let path = document.location.pathname;
|
||||||
console.log.apply(null, log("router: path ", path));
|
console.log.apply(null, log("router: path ", path));
|
||||||
|
|
||||||
|
if (path === '/notifications') {
|
||||||
|
this.router.route = App.Route.NOTIFICATIONS;
|
||||||
|
this.router.userID = '';
|
||||||
|
this.router.postID = '';
|
||||||
|
this.router.mediaID = '';
|
||||||
|
console.log.apply(null, log("router: NOTIFICATIONS"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))";
|
const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))";
|
||||||
|
|
||||||
const match = path.match(new RegExp(regex));
|
const match = path.match(new RegExp(regex));
|
||||||
@@ -1653,6 +1911,11 @@ export class App {
|
|||||||
this.router.route = App.Route.USER;
|
this.router.route = App.Route.USER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.router.route = App.Route.HOME;
|
||||||
|
this.router.userID = '';
|
||||||
|
this.router.postID = '';
|
||||||
|
this.router.mediaID = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route]));
|
console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route]));
|
||||||
@@ -1676,5 +1939,6 @@ export namespace App {
|
|||||||
GROUP = 8,
|
GROUP = 8,
|
||||||
HOME = 16,
|
HOME = 16,
|
||||||
CONNECT = 32,
|
CONNECT = 32,
|
||||||
|
NOTIFICATIONS = 64,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -39,6 +39,20 @@ export async function compressString(input: string) {
|
|||||||
return compressedArray;
|
return compressedArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function decompressBuffer(input: ArrayBuffer): Promise<string> {
|
||||||
|
const decompressionStream = new DecompressionStream('gzip');
|
||||||
|
const writer = decompressionStream.writable.getWriter();
|
||||||
|
writer.write(new Uint8Array(input));
|
||||||
|
writer.close();
|
||||||
|
const decompressedBuffer = await new Response(decompressionStream.readable).arrayBuffer();
|
||||||
|
return new TextDecoder().decode(decompressedBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function base64ToArrayBuffer(base64: string): Promise<ArrayBuffer> {
|
||||||
|
const response = await fetch("data:application/octet-stream;base64," + base64);
|
||||||
|
return response.arrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
// Base58 character set
|
// Base58 character set
|
||||||
// const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
// const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||||
// Base58 encoding
|
// Base58 encoding
|
||||||
|
|||||||
106
src/db.ts
106
src/db.ts
@@ -463,6 +463,112 @@ export async function getAllIds(userID: string): Promise<any | undefined> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function buildReplyCountMap(): Promise<{ direct: Map<string, number>, recursive: Map<string, number> }> {
|
||||||
|
const children = new Map<string, string[]>();
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', '')).filter(Boolean) as string[];
|
||||||
|
for (const userID of knownUsers) {
|
||||||
|
try {
|
||||||
|
const { store } = await getDBTransactionStore(userID);
|
||||||
|
const index = store.index("postReplyIndex");
|
||||||
|
const replies: any[] = await new Promise((resolve, reject) => {
|
||||||
|
const req = index.getAll();
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
for (const r of replies) {
|
||||||
|
const parentID = r.data.reply_to_id;
|
||||||
|
const replyID = r.data.post_id;
|
||||||
|
if (parentID && replyID) {
|
||||||
|
if (!children.has(parentID)) children.set(parentID, []);
|
||||||
|
children.get(parentID)!.push(replyID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const direct = new Map<string, number>();
|
||||||
|
for (const [parentID, kids] of children) {
|
||||||
|
direct.set(parentID, kids.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const countDescendants = (postID: string): number => {
|
||||||
|
const kids = children.get(postID) ?? [];
|
||||||
|
return kids.reduce((sum, kid) => sum + 1 + countDescendants(kid), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const recursive = new Map<string, number>();
|
||||||
|
for (const postID of children.keys()) {
|
||||||
|
recursive.set(postID, countDescendants(postID));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { direct, recursive };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPostById(postID: string): Promise<any | undefined> {
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', '')).filter(Boolean) as string[];
|
||||||
|
for (const userID of knownUsers) {
|
||||||
|
try {
|
||||||
|
const { store } = await getDBTransactionStore(userID);
|
||||||
|
const index = store.index("postIDIndex");
|
||||||
|
const result: any = await new Promise((resolve, reject) => {
|
||||||
|
const req = index.get(postID);
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
if (result) return result;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRepliesForPost(postID: string): Promise<any[]> {
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', '')).filter(Boolean) as string[];
|
||||||
|
let replies: any[] = [];
|
||||||
|
for (const userID of knownUsers) {
|
||||||
|
try {
|
||||||
|
const { store } = await getDBTransactionStore(userID);
|
||||||
|
const index = store.index("postReplyIndex");
|
||||||
|
const results: any[] = await new Promise((resolve, reject) => {
|
||||||
|
const req = index.getAll(postID);
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
replies = replies.concat(results);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return replies;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNotificationsForUser(userID: string): Promise<any[]> {
|
||||||
|
const userPostIds = new Set(await getAllIds(userID));
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', '')).filter(Boolean) as string[];
|
||||||
|
let notifications: any[] = [];
|
||||||
|
for (const dbUserID of knownUsers) {
|
||||||
|
try {
|
||||||
|
const { store } = await getDBTransactionStore(dbUserID);
|
||||||
|
const index = store.index("postReplyIndex");
|
||||||
|
const results: any[] = await new Promise((resolve, reject) => {
|
||||||
|
const req = index.getAll();
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.data.reply_to_id && userPostIds.has(r.data.reply_to_id)) {
|
||||||
|
notifications.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
notifications.sort((a, b) =>
|
||||||
|
new Date(b.data.post_timestamp).getTime() - new Date(a.data.post_timestamp).getTime()
|
||||||
|
);
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPostsByIds(userID: string, postIDs: string[]) {
|
export async function getPostsByIds(userID: string, postIDs: string[]) {
|
||||||
const { store } = await getDBTransactionStore(userID);
|
const { store } = await getDBTransactionStore(userID);
|
||||||
const index = store.index("postIDIndex");
|
const index = store.index("postIDIndex");
|
||||||
|
|||||||
104
src/sw.ts
104
src/sw.ts
@@ -38,102 +38,44 @@ self.addEventListener("install", (e: any) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
async function staleWhileRevalidate(event: any) {
|
async function fetchAndUpdateCache(request: Request): Promise<Response> {
|
||||||
|
debugLog && console.log('Service Worker: Fetching', request.url);
|
||||||
let cache = await caches.open(cacheName);
|
let networkResponse: Response;
|
||||||
|
|
||||||
let response = await cache.match(event.request);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
debugLog ? console.log('Service Worker: Cache hit', event.request.url) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchPromise = (async () => {
|
|
||||||
debugLog ? console.log('Service Worker: Fetching', event.request.url) : null;
|
|
||||||
|
|
||||||
let networkResponse = null;
|
|
||||||
try {
|
try {
|
||||||
networkResponse = await fetch(event.request);
|
networkResponse = await fetch(request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog ? console.log('Service Worker: Failed to fetch', e) : null;
|
debugLog && console.log('Service Worker: Failed to fetch', e);
|
||||||
|
|
||||||
return new Response('Network error occurred', {
|
return new Response('Network error occurred', {
|
||||||
status: 404,
|
status: 404,
|
||||||
statusText: 'Cache miss and fetch failed',
|
statusText: 'Cache miss and fetch failed',
|
||||||
headers: { 'Content-Type': 'text/plain' }
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
debugLog && console.log('Service Worker: Updating cache', request.url);
|
||||||
debugLog ? console.log('Service Worker: Updating cache', event.request.url) : null;
|
const cache = await caches.open(cacheName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cache.put(event.request, networkResponse.clone());
|
await cache.put(request, networkResponse.clone());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog ? console.log('Service Worker: failed to update cache', event.request.url, e) : null;
|
debugLog && console.log('Service Worker: failed to update cache', request.url, e);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog ? console.log('Service Worker: Returning networkResponse', event.request.url) : null;
|
|
||||||
return networkResponse;
|
return networkResponse;
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
debugLog ? console.log('Service Worker: Returning return response || fetchPromise', event.request.url) : null;
|
|
||||||
return response || fetchPromise;
|
|
||||||
|
|
||||||
// if (networkResponse) {
|
|
||||||
// cache.put(event.request, networkResponse.clone())
|
|
||||||
// return networkResponse;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// caches.open(cacheName)
|
|
||||||
// .then(function (cache) {
|
|
||||||
// return cache.match(event.request)
|
|
||||||
// .then(function (response) {
|
|
||||||
// var fetchPromise = fetch(event.request)
|
|
||||||
// .then(function (networkResponse) {
|
|
||||||
// cache.put(event.request, networkResponse.clone());
|
|
||||||
// return networkResponse;
|
|
||||||
// });
|
|
||||||
// return response || fetchPromise;
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// async function responder(event: any) {
|
|
||||||
// debugLog ? console.log('Fetching', event.request.url) : null;
|
|
||||||
|
|
||||||
// let response = await fetch(event.request);
|
|
||||||
|
|
||||||
// if (!response) {
|
|
||||||
// debugLog ? console.log('Fetch failed, falling back to cache', event.request.url) : null;
|
|
||||||
// let cacheMatch = await caches.match(event.request);
|
|
||||||
// if (!cacheMatch) {
|
|
||||||
// // DUnno what to return here!
|
|
||||||
// }
|
|
||||||
// return cacheMatch;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (response.status === 206) {
|
|
||||||
// debugLog ? console.log('Not caching partial content') : null;
|
|
||||||
// return response;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// debugLog ? console.log('Fetch successful, updating cache', event.request.url) : null;
|
|
||||||
// const cache = await caches.open(cacheName);
|
|
||||||
// try {
|
|
||||||
// cache.put(event.request, response.clone()).catch((error) => debugLog ? console.log('failed to cache', event.request, error)) : null;
|
|
||||||
// } catch (e) {
|
|
||||||
// console.log('failed to cache', event.request)
|
|
||||||
// }
|
|
||||||
// return response;
|
|
||||||
// }
|
|
||||||
|
|
||||||
self.addEventListener('fetch', function (event: any) {
|
self.addEventListener('fetch', function (event: any) {
|
||||||
event.respondWith(staleWhileRevalidate(event));
|
const networkFetch = fetchAndUpdateCache(event.request);
|
||||||
// event.respondWith(responder(event));
|
|
||||||
|
event.respondWith(async function () {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
const cached = await cache.match(event.request);
|
||||||
|
if (cached) {
|
||||||
|
debugLog && console.log('Service Worker: Cache hit', event.request.url);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
return networkFetch;
|
||||||
|
}());
|
||||||
|
|
||||||
|
// Keep the SW alive until the background cache update finishes
|
||||||
|
event.waitUntil(networkFetch);
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener("message", async (e) => {
|
addEventListener("message", async (e) => {
|
||||||
|
|||||||
299
static/App.js
299
static/App.js
@@ -1,11 +1,11 @@
|
|||||||
import { generateID } from "IDUtils";
|
import { generateID } from "IDUtils";
|
||||||
import { PeerManager, PeerEventTypes } from "PeerManager";
|
import { PeerManager, PeerEventTypes } from "PeerManager";
|
||||||
import { Sync } from "Sync";
|
import { Sync } from "Sync";
|
||||||
import { openDatabase, getData, addData, deleteData, getAllData, getPostForUser } from "db";
|
import { openDatabase, getData, addData, deleteData, mergeDataArray, getAllData, getPostForUser, getPostById, getRepliesForPost, buildReplyCountMap, getNotificationsForUser } from "db";
|
||||||
import { arrayBufferToBase64, compressString } from "dataUtils";
|
import { arrayBufferToBase64, compressString, decompressBuffer, base64ToArrayBuffer } from "dataUtils";
|
||||||
import { log, logID, renderLog, setLogVisibility } from "log";
|
import { log, logID, renderLog, setLogVisibility } from "log";
|
||||||
class Post {
|
class Post {
|
||||||
constructor(author, author_id, text, post_timestamp, imageData = null, importedFrom = null, importSource = null, reply_to_id = null) {
|
constructor(author, author_id, text, post_timestamp, imageData = null, importedFrom = null, importSource = null, reply_to_id = null, root_id = null) {
|
||||||
this.post_timestamp = post_timestamp;
|
this.post_timestamp = post_timestamp;
|
||||||
this.post_id = generateID();
|
this.post_id = generateID();
|
||||||
this.author = author;
|
this.author = author;
|
||||||
@@ -15,6 +15,7 @@ class Post {
|
|||||||
this.importedFrom = importedFrom;
|
this.importedFrom = importedFrom;
|
||||||
this.importSource = importSource;
|
this.importSource = importSource;
|
||||||
this.reply_to_id = reply_to_id;
|
this.reply_to_id = reply_to_id;
|
||||||
|
this.root_id = root_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class StatusBar {
|
class StatusBar {
|
||||||
@@ -70,6 +71,7 @@ export class App {
|
|||||||
this.userID = '';
|
this.userID = '';
|
||||||
this.peerID = '';
|
this.peerID = '';
|
||||||
this.replyToID = null;
|
this.replyToID = null;
|
||||||
|
this.replyRootID = null;
|
||||||
this.following = new Set();
|
this.following = new Set();
|
||||||
this.posts = [];
|
this.posts = [];
|
||||||
this.isHeadless = false;
|
this.isHeadless = false;
|
||||||
@@ -382,12 +384,62 @@ export class App {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
globalThis.URL.revokeObjectURL(url);
|
globalThis.URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
async importPostsForUser(userID, posts) {
|
async importPostsForUser(buffer) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
console.log.apply(null, log("Importing posts"));
|
||||||
|
const json = await decompressBuffer(buffer);
|
||||||
|
const data = JSON.parse(json);
|
||||||
|
let postsByUser;
|
||||||
|
let username = this.username;
|
||||||
|
let userID = this.userID;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
console.log.apply(null, log("Detected old export format"));
|
||||||
|
postsByUser = { [this.userID]: data };
|
||||||
}
|
}
|
||||||
async exportPostsForUser(userID) {
|
else {
|
||||||
let posts = await getAllData(userID);
|
console.log.apply(null, log("Detected new export format"));
|
||||||
let output = [];
|
const { username: importedUsername, userID: importedUserID, posts } = data;
|
||||||
console.log.apply(null, log("Serializing images"));
|
username = importedUsername;
|
||||||
|
userID = importedUserID;
|
||||||
|
postsByUser = posts;
|
||||||
|
localStorage.setItem("dandelion_username", username);
|
||||||
|
localStorage.setItem("dandelion_id", userID);
|
||||||
|
}
|
||||||
|
let totalPostsImported = 0;
|
||||||
|
const userTimings = {};
|
||||||
|
for (const [sourceUserID, posts] of Object.entries(postsByUser)) {
|
||||||
|
const userStartTime = performance.now();
|
||||||
|
const postList = posts;
|
||||||
|
for (let post of postList) {
|
||||||
|
if (post.image_data && typeof post.image_data === 'string') {
|
||||||
|
post.image_data = await base64ToArrayBuffer(post.image_data);
|
||||||
|
}
|
||||||
|
if (post.post_timestamp && typeof post.post_timestamp === 'string') {
|
||||||
|
post.post_timestamp = new Date(post.post_timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await mergeDataArray(sourceUserID, postList);
|
||||||
|
totalPostsImported += postList.length;
|
||||||
|
userTimings[sourceUserID] = performance.now() - userStartTime;
|
||||||
|
}
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const userTimingsLog = Object.entries(userTimings)
|
||||||
|
.map(([uid, ms]) => `${logID(uid)}: ${ms.toFixed(2)}ms`)
|
||||||
|
.join(', ');
|
||||||
|
console.log.apply(null, log(`Imported ${totalPostsImported} posts from ${Object.keys(postsByUser).length} users in ${totalTime.toFixed(2)}ms (${userTimingsLog})`));
|
||||||
|
}
|
||||||
|
async exportPostsForUser() {
|
||||||
|
console.log.apply(null, log("Exporting all posts for all users"));
|
||||||
|
const exportStartTime = performance.now();
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', ''))
|
||||||
|
.filter((userID) => userID !== undefined);
|
||||||
|
const postsByUser = {};
|
||||||
|
const userTimings = {};
|
||||||
|
for (const userID of knownUsers) {
|
||||||
|
const userStartTime = performance.now();
|
||||||
|
const posts = await getAllData(userID);
|
||||||
|
const output = [];
|
||||||
for (let post of posts) {
|
for (let post of posts) {
|
||||||
let newPost = post.data;
|
let newPost = post.data;
|
||||||
if (newPost.image_data) {
|
if (newPost.image_data) {
|
||||||
@@ -395,10 +447,25 @@ export class App {
|
|||||||
}
|
}
|
||||||
output.push(newPost);
|
output.push(newPost);
|
||||||
}
|
}
|
||||||
let compressedData = await compressString(JSON.stringify(output));
|
if (output.length > 0) {
|
||||||
|
postsByUser[userID] = output;
|
||||||
|
}
|
||||||
|
userTimings[userID] = performance.now() - userStartTime;
|
||||||
|
}
|
||||||
|
const totalTime = performance.now() - exportStartTime;
|
||||||
|
const userTimingsLog = Object.entries(userTimings)
|
||||||
|
.map(([uid, ms]) => `${logID(uid)}: ${ms.toFixed(2)}ms`)
|
||||||
|
.join(', ');
|
||||||
|
console.log.apply(null, log(`Exported ${Object.keys(postsByUser).length} users in ${totalTime.toFixed(2)}ms (${userTimingsLog})`));
|
||||||
|
const exportData = {
|
||||||
|
username: this.username,
|
||||||
|
userID: this.userID,
|
||||||
|
posts: postsByUser
|
||||||
|
};
|
||||||
|
let compressedData = await compressString(JSON.stringify(exportData));
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
const timestamp = `${d.getFullYear()}_${String(d.getMonth() + 1).padStart(2, '0')}_${String(d.getDate()).padStart(2, '0')}_${String(d.getHours()).padStart(2, '0')}_${String(d.getMinutes()).padStart(2, '0')}_${String(d.getSeconds()).padStart(2, '0')}`;
|
const timestamp = `${d.getFullYear()}_${String(d.getMonth() + 1).padStart(2, '0')}_${String(d.getDate()).padStart(2, '0')}_${String(d.getHours()).padStart(2, '0')}_${String(d.getMinutes()).padStart(2, '0')}_${String(d.getSeconds()).padStart(2, '0')}`;
|
||||||
this.downloadBinary(compressedData, `ddln_${this.username}_export_${timestamp}.json.gz`);
|
this.downloadBinary(compressedData, `ddln_export_${timestamp}.json.gz`);
|
||||||
}
|
}
|
||||||
async importTweetArchive(userID, tweetArchive) {
|
async importTweetArchive(userID, tweetArchive) {
|
||||||
console.log.apply(null, log("Importing tweet archive"));
|
console.log.apply(null, log("Importing tweet archive"));
|
||||||
@@ -525,7 +592,7 @@ export class App {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async createPost(userID, postText, mediaData, mimeType, replyToID = null) {
|
async createPost(userID, postText, mediaData, mimeType, replyToID = null, replyRootID = null) {
|
||||||
if ((typeof postText !== "string") || postText.length === 0) {
|
if ((typeof postText !== "string") || postText.length === 0) {
|
||||||
console.log.apply(null, log("Not posting an empty string..."));
|
console.log.apply(null, log("Not posting an empty string..."));
|
||||||
return;
|
return;
|
||||||
@@ -538,7 +605,7 @@ export class App {
|
|||||||
mediaData = compressedImage;
|
mediaData = compressedImage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID);
|
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID, replyRootID);
|
||||||
// this.posts.push(post);
|
// this.posts.push(post);
|
||||||
// localStorage.setItem(key, JSON.stringify(posts));
|
// localStorage.setItem(key, JSON.stringify(posts));
|
||||||
addData(userID, post);
|
addData(userID, post);
|
||||||
@@ -679,6 +746,14 @@ export class App {
|
|||||||
correctLevel: QRCode.CorrectLevel.H
|
correctLevel: QRCode.CorrectLevel.H
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
hideInfo() {
|
||||||
|
const infoElement = document.getElementById('info');
|
||||||
|
if (infoElement && infoElement.style.display !== 'none') {
|
||||||
|
infoElement.style.display = 'none';
|
||||||
|
setLogVisibility(false);
|
||||||
|
this.showLog = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
showInfo() {
|
showInfo() {
|
||||||
let infoElement = document.getElementById('info');
|
let infoElement = document.getElementById('info');
|
||||||
if (infoElement === null) {
|
if (infoElement === null) {
|
||||||
@@ -715,6 +790,14 @@ export class App {
|
|||||||
homeButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/`);
|
homeButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/`);
|
||||||
let profileButton = this.div('profile-button');
|
let profileButton = this.div('profile-button');
|
||||||
profileButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/user/${this.userID}`);
|
profileButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/user/${this.userID}`);
|
||||||
|
let notificationsButton = this.div('notifications-button');
|
||||||
|
notificationsButton.addEventListener('click', () => {
|
||||||
|
this.hideInfo();
|
||||||
|
navContainer.classList.remove('active');
|
||||||
|
history.pushState({}, '', '/notifications');
|
||||||
|
this.getRoute();
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
let monitorButton = this.div('monitor_button');
|
let monitorButton = this.div('monitor_button');
|
||||||
monitorButton.addEventListener('click', async () => {
|
monitorButton.addEventListener('click', async () => {
|
||||||
navContainer.classList.toggle('active');
|
navContainer.classList.toggle('active');
|
||||||
@@ -723,9 +806,21 @@ export class App {
|
|||||||
let navContainer = this.div('nav-container');
|
let navContainer = this.div('nav-container');
|
||||||
let burgerMenuButton = this.div('burger-menu-button');
|
let burgerMenuButton = this.div('burger-menu-button');
|
||||||
burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
|
burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
|
||||||
|
let importFilePicker = document.getElementById('import-file-input');
|
||||||
|
importFilePicker?.addEventListener('change', async () => {
|
||||||
|
const file = importFilePicker.files?.[0];
|
||||||
|
if (!file)
|
||||||
|
return;
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
await this.importPostsForUser(buffer);
|
||||||
|
importFilePicker.value = '';
|
||||||
|
this.userID = localStorage.getItem("dandelion_id") || this.userID;
|
||||||
|
this.username = localStorage.getItem("dandelion_username") || this.username;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
let exportButton = this.button("export-button");
|
let exportButton = this.button("export-button");
|
||||||
exportButton.addEventListener('click', async (e) => {
|
exportButton.addEventListener('click', async (e) => {
|
||||||
await this.exportPostsForUser(this.userID);
|
await this.exportPostsForUser();
|
||||||
});
|
});
|
||||||
let composeButton = this.div('compose-button');
|
let composeButton = this.div('compose-button');
|
||||||
composeButton.addEventListener('click', e => {
|
composeButton.addEventListener('click', e => {
|
||||||
@@ -735,7 +830,7 @@ export class App {
|
|||||||
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();
|
||||||
await this.createPost(this.userID, 'image...', buffer, file.type);
|
await this.createPost(this.userID, 'image...', buffer, file.type, this.replyToID, this.replyRootID);
|
||||||
}
|
}
|
||||||
// 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 = '';
|
||||||
@@ -779,11 +874,18 @@ export class App {
|
|||||||
const dataTransfer = e.clipboardData;
|
const dataTransfer = e.clipboardData;
|
||||||
const file = dataTransfer.files[0];
|
const file = dataTransfer.files[0];
|
||||||
let buffer = await file.arrayBuffer();
|
let buffer = await file.arrayBuffer();
|
||||||
await this.createPost(this.userID, 'image...', buffer, file.type);
|
await this.createPost(this.userID, 'image...', buffer, file.type, this.replyToID, this.replyRootID);
|
||||||
});
|
});
|
||||||
postButton.addEventListener("click", () => {
|
const submitPost = () => {
|
||||||
this.createPost(userID, postText.value, undefined, undefined, this.replyToID);
|
this.createPost(userID, postText.value, undefined, undefined, this.replyToID, this.replyRootID);
|
||||||
this.exitCompose();
|
this.exitCompose();
|
||||||
|
};
|
||||||
|
postButton.addEventListener("click", submitPost);
|
||||||
|
postText.addEventListener("keydown", (e) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
submitPost();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// updateApp.addEventListener("click", () => {
|
// updateApp.addEventListener("click", () => {
|
||||||
// registration?.active?.postMessage({ type: "update_app" });
|
// registration?.active?.postMessage({ type: "update_app" });
|
||||||
@@ -793,18 +895,21 @@ export class App {
|
|||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
// Change this all to a template so we're not toggling state in this crazy way!
|
// Change this all to a template so we're not toggling state in this crazy way!
|
||||||
enterCompose(replyToID = null) {
|
enterCompose(replyToID = null, replyRootID = null) {
|
||||||
|
this.replyToID = replyToID;
|
||||||
|
this.replyRootID = replyRootID;
|
||||||
if (replyToID) {
|
if (replyToID) {
|
||||||
this.renderComposeReplyArea(replyToID);
|
this.renderComposeReplyArea(replyToID);
|
||||||
document.getElementById("compose-reply-area").style.display = "block";
|
document.getElementById("compose-reply-area").style.display = "block";
|
||||||
}
|
}
|
||||||
replyToID = replyToID;
|
document.getElementById('compose').style.display = 'flex';
|
||||||
document.getElementById('compose').style.display = 'block';
|
|
||||||
document.getElementById('textarea_post')?.focus();
|
document.getElementById('textarea_post')?.focus();
|
||||||
document.getElementById('compose-dimmer')?.classList.add("compose-dimmer-dimmed");
|
document.getElementById('compose-dimmer')?.classList.add("compose-dimmer-dimmed");
|
||||||
document.body.classList.add("no-scroll");
|
document.body.classList.add("no-scroll");
|
||||||
}
|
}
|
||||||
exitCompose() {
|
exitCompose() {
|
||||||
|
this.replyToID = null;
|
||||||
|
this.replyRootID = null;
|
||||||
let postText = document.getElementById("textarea_post");
|
let postText = document.getElementById("textarea_post");
|
||||||
postText.value = "";
|
postText.value = "";
|
||||||
document.getElementById('compose').style.display = 'none';
|
document.getElementById('compose').style.display = 'none';
|
||||||
@@ -940,6 +1045,7 @@ export class App {
|
|||||||
this.sync.setArchive(this.isArchivePeer);
|
this.sync.setArchive(this.isArchivePeer);
|
||||||
this.connect();
|
this.connect();
|
||||||
await this.initDB();
|
await this.initDB();
|
||||||
|
window.addEventListener('popstate', () => { this.hideInfo(); this.getRoute(); this.render(); });
|
||||||
this.connectURL = `${document.location.origin}/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 time = 0;
|
let time = 0;
|
||||||
@@ -1040,6 +1146,11 @@ export class App {
|
|||||||
compose.style.display = "none";
|
compose.style.display = "none";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case App.Route.NOTIFICATIONS: {
|
||||||
|
this.posts = await getNotificationsForUser(this.userID);
|
||||||
|
document.getElementById('compose').style.display = "none";
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
console.log.apply(null, log("Render: got a route I didn't understand. Rendering HOME:", this.router.route));
|
console.log.apply(null, log("Render: got a route I didn't understand. Rendering HOME:", this.router.route));
|
||||||
this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
|
this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
|
||||||
@@ -1050,6 +1161,20 @@ export class App {
|
|||||||
if (!contentDiv) {
|
if (!contentDiv) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
if (this.router.route === App.Route.NOTIFICATIONS) {
|
||||||
|
contentDiv.innerHTML = '';
|
||||||
|
if (this.posts.length === 0) {
|
||||||
|
contentDiv.innerHTML = '<div style="padding:20px;color:gray">No notifications yet.</div>';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let first = true;
|
||||||
|
for (const postRecord of this.posts) {
|
||||||
|
contentDiv.appendChild(this.renderNotificationItem(postRecord.data, first));
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.posts.length === 0) {
|
if (this.posts.length === 0) {
|
||||||
this.renderWelcome(contentDiv);
|
this.renderWelcome(contentDiv);
|
||||||
return;
|
return;
|
||||||
@@ -1072,13 +1197,18 @@ export class App {
|
|||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
contentDiv.innerHTML = "";
|
contentDiv.innerHTML = "";
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
const isPostView = this.router.route === App.Route.POST;
|
||||||
|
contentDiv.classList.toggle('post-view', isPostView);
|
||||||
|
const replyCountMap = await buildReplyCountMap();
|
||||||
this.renderedPosts.clear();
|
this.renderedPosts.clear();
|
||||||
let first = true;
|
let first = true;
|
||||||
for (let i = this.posts.length - 1; i >= 0; i--) {
|
for (let i = this.posts.length - 1; i >= 0; i--) {
|
||||||
let postData = this.posts[i];
|
let postData = this.posts[i];
|
||||||
|
if (!isPostView && postData.data.reply_to_id)
|
||||||
|
continue;
|
||||||
// this.postsSet.add(postData);
|
// this.postsSet.add(postData);
|
||||||
// TODO return promises for all image loads and await those.
|
// TODO return promises for all image loads and await those.
|
||||||
let post = this.renderPost(postData.data, first);
|
let post = this.renderPost(postData.data, first, replyCountMap.recursive.get(postData.data.post_id) ?? 0);
|
||||||
first = false;
|
first = false;
|
||||||
// this.renderedPosts.set(postData.post_id, post);
|
// this.renderedPosts.set(postData.post_id, post);
|
||||||
if (post) {
|
if (post) {
|
||||||
@@ -1093,6 +1223,36 @@ export class App {
|
|||||||
throw new Error("Couldn't get content div!");
|
throw new Error("Couldn't get content div!");
|
||||||
}
|
}
|
||||||
contentDiv.appendChild(fragment);
|
contentDiv.appendChild(fragment);
|
||||||
|
if (isPostView && this.posts.length > 0) {
|
||||||
|
const currentPost = this.posts[0].data;
|
||||||
|
if (currentPost.root_id) {
|
||||||
|
const rootRecord = await getPostById(currentPost.root_id);
|
||||||
|
if (rootRecord) {
|
||||||
|
const rootEl = this.renderPost(rootRecord.data, true, replyCountMap.recursive.get(rootRecord.data.post_id) ?? 0);
|
||||||
|
contentDiv.insertBefore(rootEl, contentDiv.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const renderReplies = async (postID, depth) => {
|
||||||
|
if (depth > 2)
|
||||||
|
return;
|
||||||
|
const replies = await getRepliesForPost(postID);
|
||||||
|
// Top-level replies: newest first. Nested replies: oldest first (readable order).
|
||||||
|
if (depth === 0) {
|
||||||
|
replies.sort((a, b) => new Date(b.data.post_timestamp).getTime() - new Date(a.data.post_timestamp).getTime());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
replies.sort((a, b) => new Date(a.data.post_timestamp).getTime() - new Date(b.data.post_timestamp).getTime());
|
||||||
|
}
|
||||||
|
for (const reply of replies) {
|
||||||
|
const el = this.renderPost(reply.data, false, replyCountMap.direct.get(reply.data.post_id) ?? 0);
|
||||||
|
if (depth > 0)
|
||||||
|
el.style.marginLeft = `${depth * 20}px`;
|
||||||
|
contentDiv.appendChild(el);
|
||||||
|
await renderReplies(reply.data.post_id, depth + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await renderReplies(this.posts[0].data.post_id, currentPost.root_id ? 1 : 0);
|
||||||
|
}
|
||||||
let renderTime = this.timerDelta();
|
let renderTime = this.timerDelta();
|
||||||
console.log.apply(null, log(`render took: ${renderTime.toFixed(2)}ms`));
|
console.log.apply(null, log(`render took: ${renderTime.toFixed(2)}ms`));
|
||||||
;
|
;
|
||||||
@@ -1106,12 +1266,63 @@ export class App {
|
|||||||
deleteData(userID, postID);
|
deleteData(userID, postID);
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
renderComposeReplyArea(replyToID) {
|
async renderComposeReplyArea(replyToID) {
|
||||||
let composeReplyArea = document.getElementById('compose-reply-area');
|
const composeReplyArea = document.getElementById('compose-reply-area');
|
||||||
composeReplyArea.innerText = replyToID;
|
composeReplyArea.innerHTML = '';
|
||||||
composeReplyArea.classList.add("show");
|
const record = await getPostById(replyToID);
|
||||||
|
if (!record)
|
||||||
|
return;
|
||||||
|
const postData = record.data;
|
||||||
|
const authorDiv = document.createElement('div');
|
||||||
|
authorDiv.className = 'compose-reply-author';
|
||||||
|
authorDiv.textContent = `@${postData.author}`;
|
||||||
|
composeReplyArea.appendChild(authorDiv);
|
||||||
|
const previewDiv = document.createElement('div');
|
||||||
|
previewDiv.className = 'compose-reply-preview';
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
textDiv.className = 'compose-reply-preview-text';
|
||||||
|
textDiv.innerHTML = this.markedAvailable ? marked.parse(postData.text) : postData.text;
|
||||||
|
previewDiv.appendChild(textDiv);
|
||||||
|
if (postData.image_data) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.className = 'compose-reply-preview-image';
|
||||||
|
const blob = new Blob([postData.image_data]);
|
||||||
|
img.src = URL.createObjectURL(blob);
|
||||||
|
previewDiv.appendChild(img);
|
||||||
}
|
}
|
||||||
renderPost(post, first) {
|
composeReplyArea.appendChild(previewDiv);
|
||||||
|
}
|
||||||
|
renderNotificationItem(post, first) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.style.cssText = 'padding: 8px 10px; cursor: pointer;';
|
||||||
|
if (!first)
|
||||||
|
el.innerHTML = '<hr>';
|
||||||
|
const authorDiv = document.createElement('div');
|
||||||
|
authorDiv.className = 'compose-reply-author';
|
||||||
|
authorDiv.textContent = `@${post.author} · ${post.post_timestamp.toLocaleDateString()}`;
|
||||||
|
el.appendChild(authorDiv);
|
||||||
|
const previewDiv = document.createElement('div');
|
||||||
|
previewDiv.className = 'compose-reply-preview';
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
textDiv.className = 'compose-reply-preview-text';
|
||||||
|
textDiv.style.maxHeight = 'none';
|
||||||
|
textDiv.innerHTML = this.markedAvailable ? marked.parse(post.text) : post.text;
|
||||||
|
previewDiv.appendChild(textDiv);
|
||||||
|
if (post.image_data) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.className = 'compose-reply-preview-image';
|
||||||
|
img.src = URL.createObjectURL(new Blob([post.image_data]));
|
||||||
|
previewDiv.appendChild(img);
|
||||||
|
}
|
||||||
|
el.appendChild(previewDiv);
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
history.pushState({}, '', `/user/${post.author_id}/post/${post.post_id}`);
|
||||||
|
this.getRoute();
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
renderPost(post, first, replyCount = 0) {
|
||||||
if (!(post.hasOwnProperty("text"))) {
|
if (!(post.hasOwnProperty("text"))) {
|
||||||
throw new Error("Post is malformed!");
|
throw new Error("Post is malformed!");
|
||||||
}
|
}
|
||||||
@@ -1128,12 +1339,11 @@ export class App {
|
|||||||
await navigator.clipboard.writeText(shareUrl);
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
};
|
};
|
||||||
let replyButton = document.createElement('button');
|
let replyButton = document.createElement('button');
|
||||||
replyButton.innerText = 'reply';
|
replyButton.innerText = replyCount > 0 ? `reply (${replyCount})` : 'reply';
|
||||||
replyButton.onclick = async () => {
|
replyButton.onclick = async () => {
|
||||||
console.log(`replying to post ${post.post_id}`);
|
console.log(`replying to post ${post.post_id}`);
|
||||||
this.enterCompose(post.post_id);
|
const rootID = post.root_id ?? post.post_id;
|
||||||
// let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
|
this.enterCompose(post.post_id, rootID);
|
||||||
// await navigator.clipboard.writeText(shareUrl)
|
|
||||||
};
|
};
|
||||||
let ownPost = post.author_id === this.userID;
|
let ownPost = post.author_id === this.userID;
|
||||||
let markdown = post.text;
|
let markdown = post.text;
|
||||||
@@ -1148,7 +1358,8 @@ export 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 = `${document.location.origin}/user/${post.author_id}/`;
|
let userURL = `${document.location.origin}/user/${post.author_id}/`;
|
||||||
let postTemplate = `<div>${first ? '' : '<hr>'}
|
let postTemplate = `<div class="post-container">${first ? '' : '<hr>'}
|
||||||
|
<div class="post-body">
|
||||||
<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> -
|
||||||
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
|
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
|
||||||
@@ -1162,10 +1373,17 @@ export class App {
|
|||||||
${ownPost ? `<span id="editButton"></span>` : ''}
|
${ownPost ? `<span id="editButton"></span>` : ''}
|
||||||
<span id="shareButton"></span>
|
<span id="shareButton"></span>
|
||||||
${ownPost ? `<span id="deleteButton"></span>` : ''}
|
${ownPost ? `<span id="deleteButton"></span>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>`;
|
</div>`;
|
||||||
containerDiv.innerHTML = postTemplate;
|
containerDiv.innerHTML = postTemplate;
|
||||||
|
const postBody = containerDiv.querySelector('.post-body');
|
||||||
|
postBody.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('button, a'))
|
||||||
|
return;
|
||||||
|
history.pushState({}, '', `/user/${post.author_id}/post/${post.post_id}`);
|
||||||
|
this.getRoute();
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
if (ownPost) {
|
if (ownPost) {
|
||||||
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
|
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
|
||||||
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
||||||
@@ -1197,6 +1415,14 @@ export class App {
|
|||||||
getRoute() {
|
getRoute() {
|
||||||
let path = document.location.pathname;
|
let path = document.location.pathname;
|
||||||
console.log.apply(null, log("router: path ", path));
|
console.log.apply(null, log("router: path ", path));
|
||||||
|
if (path === '/notifications') {
|
||||||
|
this.router.route = App.Route.NOTIFICATIONS;
|
||||||
|
this.router.userID = '';
|
||||||
|
this.router.postID = '';
|
||||||
|
this.router.mediaID = '';
|
||||||
|
console.log.apply(null, log("router: NOTIFICATIONS"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))";
|
const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))";
|
||||||
const match = path.match(new RegExp(regex));
|
const match = path.match(new RegExp(regex));
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -1219,6 +1445,12 @@ export class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
this.router.route = App.Route.HOME;
|
||||||
|
this.router.userID = '';
|
||||||
|
this.router.postID = '';
|
||||||
|
this.router.mediaID = '';
|
||||||
|
}
|
||||||
console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route]));
|
console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route]));
|
||||||
// user = /user/<ID>
|
// user = /user/<ID>
|
||||||
// post = /user/<ID>/post/<ID>
|
// post = /user/<ID>/post/<ID>
|
||||||
@@ -1237,6 +1469,7 @@ export class App {
|
|||||||
Route[Route["GROUP"] = 8] = "GROUP";
|
Route[Route["GROUP"] = 8] = "GROUP";
|
||||||
Route[Route["HOME"] = 16] = "HOME";
|
Route[Route["HOME"] = 16] = "HOME";
|
||||||
Route[Route["CONNECT"] = 32] = "CONNECT";
|
Route[Route["CONNECT"] = 32] = "CONNECT";
|
||||||
|
Route[Route["NOTIFICATIONS"] = 64] = "NOTIFICATIONS";
|
||||||
})(Route = App.Route || (App.Route = {}));
|
})(Route = App.Route || (App.Route = {}));
|
||||||
})(App || (App = {}));
|
})(App || (App = {}));
|
||||||
;
|
;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -31,6 +31,18 @@ export async function compressString(input) {
|
|||||||
// Convert the compressed data to a Uint8Array
|
// Convert the compressed data to a Uint8Array
|
||||||
return compressedArray;
|
return compressedArray;
|
||||||
}
|
}
|
||||||
|
export async function decompressBuffer(input) {
|
||||||
|
const decompressionStream = new DecompressionStream('gzip');
|
||||||
|
const writer = decompressionStream.writable.getWriter();
|
||||||
|
writer.write(new Uint8Array(input));
|
||||||
|
writer.close();
|
||||||
|
const decompressedBuffer = await new Response(decompressionStream.readable).arrayBuffer();
|
||||||
|
return new TextDecoder().decode(decompressedBuffer);
|
||||||
|
}
|
||||||
|
export async function base64ToArrayBuffer(base64) {
|
||||||
|
const response = await fetch("data:application/octet-stream;base64," + base64);
|
||||||
|
return response.arrayBuffer();
|
||||||
|
}
|
||||||
// Base58 character set
|
// Base58 character set
|
||||||
// const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
// const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||||
// Base58 encoding
|
// Base58 encoding
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"dataUtils.js","sourceRoot":"","sources":["../src/dataUtils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAA8B,EAAE,IAAI,GAAG,0BAA0B;IAC1G,OAAO,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,UAAU,EAAE,EAAE;YAC7C,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;YACpC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;SACpC,CAAC,CAAC;QACH,MAAM,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAmB;IAC3D,IAAI,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IACnC,OAAO,CAAC,MAAM,oBAAoB,CAAC,KAAK,CAAY,CAAA,CAAC,OAAO,CAAC,uCAAuC,EAAE,EAAE,CAAC,CAAC;AAC5G,CAAC;AAED,6DAA6D;AAC7D,wFAAwF;AACxF,oDAAoD;AACpD,wBAAwB;AACxB,IAAI;AAEJ,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAa;IAChD,qCAAqC;IACrC,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;IACtC,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE7C,6BAA6B;IAC7B,MAAM,iBAAiB,GAAG,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;IAEtD,sCAAsC;IACtC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACzB,MAAM,CAAC,KAAK,EAAE,CAAC;IAEf,2CAA2C;IAC3C,MAAM,eAAe,GAAG,MAAM,IAAI,QAAQ,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IAErF,8CAA8C;IAC9C,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,uBAAuB;AACvB,wFAAwF;AACxF,kBAAkB;AAClB,kBAAkB;AAClB,sDAAsD;AACtD,eAAe;AACf,wBAAwB;AAExB,iCAAiC;AACjC,oBAAoB;AACpB,gDAAgD;AAChD,iCAAiC;AACjC,gCAAgC;AAChC,wCAAwC;AACxC,QAAQ;AACR,0BAA0B;AAC1B,iCAAiC;AACjC,wCAAwC;AACxC,QAAQ;AACR,MAAM;AAEN,qBAAqB;AACrB,4CAA4C;AAC5C,wCAAwC;AACxC,MAAM;AAEN,iCAAiC;AACjC,iCAAiC;AACjC,2BAA2B;AAC3B,8CAA8C;AAC9C,eAAe;AACf,eAAe;AACf,QAAQ;AACR,MAAM;AAEN,mBAAmB;AACnB,IAAI;AAEJ,4BAA4B;AAC5B,gDAAgD;AAChD,qCAAqC;AACrC,gCAAgC;AAChC,IAAI"}
|
{"version":3,"file":"dataUtils.js","sourceRoot":"","sources":["../src/dataUtils.ts"],"names":[],"mappings":"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"}
|
||||||
102
static/db.js
102
static/db.js
@@ -356,6 +356,108 @@ export async function getAllIds(userID) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export async function buildReplyCountMap() {
|
||||||
|
const children = new Map();
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', '')).filter(Boolean);
|
||||||
|
for (const userID of knownUsers) {
|
||||||
|
try {
|
||||||
|
const { store } = await getDBTransactionStore(userID);
|
||||||
|
const index = store.index("postReplyIndex");
|
||||||
|
const replies = await new Promise((resolve, reject) => {
|
||||||
|
const req = index.getAll();
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
for (const r of replies) {
|
||||||
|
const parentID = r.data.reply_to_id;
|
||||||
|
const replyID = r.data.post_id;
|
||||||
|
if (parentID && replyID) {
|
||||||
|
if (!children.has(parentID))
|
||||||
|
children.set(parentID, []);
|
||||||
|
children.get(parentID).push(replyID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (_) { }
|
||||||
|
}
|
||||||
|
const direct = new Map();
|
||||||
|
for (const [parentID, kids] of children) {
|
||||||
|
direct.set(parentID, kids.length);
|
||||||
|
}
|
||||||
|
const countDescendants = (postID) => {
|
||||||
|
const kids = children.get(postID) ?? [];
|
||||||
|
return kids.reduce((sum, kid) => sum + 1 + countDescendants(kid), 0);
|
||||||
|
};
|
||||||
|
const recursive = new Map();
|
||||||
|
for (const postID of children.keys()) {
|
||||||
|
recursive.set(postID, countDescendants(postID));
|
||||||
|
}
|
||||||
|
return { direct, recursive };
|
||||||
|
}
|
||||||
|
export async function getPostById(postID) {
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', '')).filter(Boolean);
|
||||||
|
for (const userID of knownUsers) {
|
||||||
|
try {
|
||||||
|
const { store } = await getDBTransactionStore(userID);
|
||||||
|
const index = store.index("postIDIndex");
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
const req = index.get(postID);
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
if (result)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (_) { }
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
export async function getRepliesForPost(postID) {
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', '')).filter(Boolean);
|
||||||
|
let replies = [];
|
||||||
|
for (const userID of knownUsers) {
|
||||||
|
try {
|
||||||
|
const { store } = await getDBTransactionStore(userID);
|
||||||
|
const index = store.index("postReplyIndex");
|
||||||
|
const results = await new Promise((resolve, reject) => {
|
||||||
|
const req = index.getAll(postID);
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
replies = replies.concat(results);
|
||||||
|
}
|
||||||
|
catch (_) { }
|
||||||
|
}
|
||||||
|
return replies;
|
||||||
|
}
|
||||||
|
export async function getNotificationsForUser(userID) {
|
||||||
|
const userPostIds = new Set(await getAllIds(userID));
|
||||||
|
const knownUsers = [...(await indexedDB.databases())]
|
||||||
|
.map(db => db.name?.replace('user_', '')).filter(Boolean);
|
||||||
|
let notifications = [];
|
||||||
|
for (const dbUserID of knownUsers) {
|
||||||
|
try {
|
||||||
|
const { store } = await getDBTransactionStore(dbUserID);
|
||||||
|
const index = store.index("postReplyIndex");
|
||||||
|
const results = await new Promise((resolve, reject) => {
|
||||||
|
const req = index.getAll();
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.data.reply_to_id && userPostIds.has(r.data.reply_to_id)) {
|
||||||
|
notifications.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (_) { }
|
||||||
|
}
|
||||||
|
notifications.sort((a, b) => new Date(b.data.post_timestamp).getTime() - new Date(a.data.post_timestamp).getTime());
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
export async function getPostsByIds(userID, postIDs) {
|
export async function getPostsByIds(userID, postIDs) {
|
||||||
const { store } = await getDBTransactionStore(userID);
|
const { store } = await getDBTransactionStore(userID);
|
||||||
const index = store.index("postIDIndex");
|
const index = store.index("postIDIndex");
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, interactive-widget=resizes-content">
|
||||||
|
|
||||||
<title>Dandelion</title>
|
<title>Dandelion</title>
|
||||||
|
|
||||||
@@ -58,6 +58,10 @@
|
|||||||
<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>
|
||||||
|
<a class="nav-item" id="notifications-button">
|
||||||
|
<span class="nav-emoji emoji-fill" role="img" aria-label="Notifications">🔔</span>
|
||||||
|
<span class="nav-label">Notifications</span>
|
||||||
|
</a>
|
||||||
<!-- <a class="nav-item">
|
<!-- <a class="nav-item">
|
||||||
<span class="nav-emoji" role="img" aria-label="Search">🔍</span>
|
<span class="nav-emoji" role="img" aria-label="Search">🔍</span>
|
||||||
<span class="nav-label">Search</span>
|
<span class="nav-label">Search</span>
|
||||||
@@ -87,6 +91,8 @@
|
|||||||
|
|
||||||
<div id="info" style="display:none">
|
<div id="info" style="display:none">
|
||||||
<button id="export-button">export</button>
|
<button id="export-button">export</button>
|
||||||
|
<label for="import-file-input" id="import-button" class="button">import</label>
|
||||||
|
<input type="file" id="import-file-input" accept=".gz" style="display:none">
|
||||||
|
|
||||||
<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"
|
||||||
@@ -129,7 +135,10 @@
|
|||||||
|
|
||||||
|
|
||||||
<div id="compose">
|
<div id="compose">
|
||||||
|
<div id="compose-header">
|
||||||
<div id="button_cancel_post" class="link">Cancel</div>
|
<div id="button_cancel_post" class="link">Cancel</div>
|
||||||
|
<button id="button_post" class="button button-big">post</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="compose-reply-area"></div>
|
<div id="compose-reply-area"></div>
|
||||||
<textarea cols="60" rows="6" id="textarea_post"></textarea>
|
<textarea cols="60" rows="6" id="textarea_post"></textarea>
|
||||||
@@ -138,11 +147,6 @@
|
|||||||
<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">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="compose-right-buttons">
|
|
||||||
<!-- <button id="button_add_pic" >🏞️</button> -->
|
|
||||||
<button id="button_post" class="button button-big">post</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
103
static/main.css
103
static/main.css
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--main-bg-color: white;
|
--main-bg-color: white;
|
||||||
--main-hover-color: rgb(64, 64, 64) --border-color: rgb(132, 136, 138);
|
--main-hover-color: rgb(64, 64, 64);
|
||||||
|
--post-hover-color: rgb(32,32,32);
|
||||||
|
--border-color: rgb(132, 136, 138);
|
||||||
--edge-color: rgb(60, 60, 60);
|
--edge-color: rgb(60, 60, 60);
|
||||||
--main-fg-color: black;
|
--main-fg-color: black;
|
||||||
--highlight-fg-color: rgb(255, 255, 255);
|
--highlight-fg-color: rgb(255, 255, 255);
|
||||||
@@ -124,6 +126,24 @@ hr {
|
|||||||
|
|
||||||
.postImage {
|
.postImage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-view .postImage {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
transition: background-color 0.1s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body:hover {
|
||||||
|
background-color: var(--post-hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#log {
|
#log {
|
||||||
@@ -277,6 +297,7 @@ iframe {
|
|||||||
|
|
||||||
#compose {
|
#compose {
|
||||||
display: none;
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 5%;
|
top: 5%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -296,8 +317,84 @@ iframe {
|
|||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#compose-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
#compose {
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: 100dvh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: hidden;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#textarea_post {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
resize: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#textarea_post:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compose-reply-area {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#compose-reply-area {
|
#compose-reply-area {
|
||||||
display:none;
|
display: none;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-reply-author {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.85em;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-reply-preview {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-reply-preview-text {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-reply-preview-text iframe {
|
||||||
|
height: 68px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-reply-preview-image {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
max-width: 96px;
|
||||||
|
max-height: 96px;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.show {
|
.show {
|
||||||
@@ -322,7 +419,7 @@ iframe {
|
|||||||
/* 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: 130px;
|
width: 185px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
static/sw.js
81
static/sw.js
@@ -30,82 +30,43 @@ self.addEventListener("install", (e) => {
|
|||||||
}
|
}
|
||||||
})());
|
})());
|
||||||
});
|
});
|
||||||
async function staleWhileRevalidate(event) {
|
async function fetchAndUpdateCache(request) {
|
||||||
let cache = await caches.open(cacheName);
|
debugLog && console.log('Service Worker: Fetching', request.url);
|
||||||
let response = await cache.match(event.request);
|
let networkResponse;
|
||||||
if (response) {
|
|
||||||
debugLog ? console.log('Service Worker: Cache hit', event.request.url) : null;
|
|
||||||
}
|
|
||||||
const fetchPromise = (async () => {
|
|
||||||
debugLog ? console.log('Service Worker: Fetching', event.request.url) : null;
|
|
||||||
let networkResponse = null;
|
|
||||||
try {
|
try {
|
||||||
networkResponse = await fetch(event.request);
|
networkResponse = await fetch(request);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
debugLog ? console.log('Service Worker: Failed to fetch', e) : null;
|
debugLog && console.log('Service Worker: Failed to fetch', e);
|
||||||
return new Response('Network error occurred', {
|
return new Response('Network error occurred', {
|
||||||
status: 404,
|
status: 404,
|
||||||
statusText: 'Cache miss and fetch failed',
|
statusText: 'Cache miss and fetch failed',
|
||||||
headers: { 'Content-Type': 'text/plain' }
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
debugLog ? console.log('Service Worker: Updating cache', event.request.url) : null;
|
debugLog && console.log('Service Worker: Updating cache', request.url);
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
try {
|
try {
|
||||||
await cache.put(event.request, networkResponse.clone());
|
await cache.put(request, networkResponse.clone());
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
debugLog ? console.log('Service Worker: failed to update cache', event.request.url, e) : null;
|
debugLog && console.log('Service Worker: failed to update cache', request.url, e);
|
||||||
}
|
}
|
||||||
debugLog ? console.log('Service Worker: Returning networkResponse', event.request.url) : null;
|
|
||||||
return networkResponse;
|
return networkResponse;
|
||||||
})();
|
|
||||||
debugLog ? console.log('Service Worker: Returning return response || fetchPromise', event.request.url) : null;
|
|
||||||
return response || fetchPromise;
|
|
||||||
// if (networkResponse) {
|
|
||||||
// cache.put(event.request, networkResponse.clone())
|
|
||||||
// return networkResponse;
|
|
||||||
// }
|
|
||||||
// caches.open(cacheName)
|
|
||||||
// .then(function (cache) {
|
|
||||||
// return cache.match(event.request)
|
|
||||||
// .then(function (response) {
|
|
||||||
// var fetchPromise = fetch(event.request)
|
|
||||||
// .then(function (networkResponse) {
|
|
||||||
// cache.put(event.request, networkResponse.clone());
|
|
||||||
// return networkResponse;
|
|
||||||
// });
|
|
||||||
// return response || fetchPromise;
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
// async function responder(event: any) {
|
|
||||||
// debugLog ? console.log('Fetching', event.request.url) : null;
|
|
||||||
// let response = await fetch(event.request);
|
|
||||||
// if (!response) {
|
|
||||||
// debugLog ? console.log('Fetch failed, falling back to cache', event.request.url) : null;
|
|
||||||
// let cacheMatch = await caches.match(event.request);
|
|
||||||
// if (!cacheMatch) {
|
|
||||||
// // DUnno what to return here!
|
|
||||||
// }
|
|
||||||
// return cacheMatch;
|
|
||||||
// }
|
|
||||||
// if (response.status === 206) {
|
|
||||||
// debugLog ? console.log('Not caching partial content') : null;
|
|
||||||
// return response;
|
|
||||||
// }
|
|
||||||
// debugLog ? console.log('Fetch successful, updating cache', event.request.url) : null;
|
|
||||||
// const cache = await caches.open(cacheName);
|
|
||||||
// try {
|
|
||||||
// cache.put(event.request, response.clone()).catch((error) => debugLog ? console.log('failed to cache', event.request, error)) : null;
|
|
||||||
// } catch (e) {
|
|
||||||
// console.log('failed to cache', event.request)
|
|
||||||
// }
|
|
||||||
// return response;
|
|
||||||
// }
|
|
||||||
self.addEventListener('fetch', function (event) {
|
self.addEventListener('fetch', function (event) {
|
||||||
event.respondWith(staleWhileRevalidate(event));
|
const networkFetch = fetchAndUpdateCache(event.request);
|
||||||
// event.respondWith(responder(event));
|
event.respondWith(async function () {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
const cached = await cache.match(event.request);
|
||||||
|
if (cached) {
|
||||||
|
debugLog && console.log('Service Worker: Cache hit', event.request.url);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
return networkFetch;
|
||||||
|
}());
|
||||||
|
// Keep the SW alive until the background cache update finishes
|
||||||
|
event.waitUntil(networkFetch);
|
||||||
});
|
});
|
||||||
addEventListener("message", async (e) => {
|
addEventListener("message", async (e) => {
|
||||||
debugLog ? console.log(`Message received:`, e.data) : null;
|
debugLog ? console.log(`Message received:`, e.data) : null;
|
||||||
|
|||||||
@@ -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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"}
|
||||||
Reference in New Issue
Block a user