Compare commits
9 Commits
bobbyd-sho
...
c1de283fb6
| Author | SHA1 | Date | |
|---|---|---|---|
| c1de283fb6 | |||
| 0d392c90cc | |||
| 3a056dffce | |||
| 5a6691a214 | |||
| 9ff871b0e3 | |||
| dbf45dbf14 | |||
| 7c25342f82 | |||
| 96ea7d56f9 | |||
| bf3eb0ae9f |
2
README.md
Normal file
2
README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Intro
|
||||||
|
Dandelion is an experimental social network designed to let you build online communites that will last for 100 years.
|
||||||
@@ -360,11 +360,15 @@ async function main() {
|
|||||||
messageDispatch.set('hello2', hello2Handler);
|
messageDispatch.set('hello2', hello2Handler);
|
||||||
messageDispatch.set('peer_message', peerMessageHandler);
|
messageDispatch.set('peer_message', peerMessageHandler);
|
||||||
|
|
||||||
|
const port = parseInt(Deno.env.get("PORT") ?? "443");
|
||||||
|
const certFile = Deno.env.get("TLS_CERT") ?? "/etc/letsencrypt/live/ddln.app/fullchain.pem";
|
||||||
|
const keyFile = Deno.env.get("TLS_KEY") ?? "/etc/letsencrypt/live/ddln.app/privkey.pem";
|
||||||
|
|
||||||
Deno.serve({
|
Deno.serve({
|
||||||
hostname: "[::]",
|
hostname: "[::]",
|
||||||
port: 443,
|
port,
|
||||||
cert: Deno.readTextFileSync("/etc/letsencrypt/live/ddln.app/fullchain.pem"),
|
cert: Deno.readTextFileSync(certFile),
|
||||||
key: Deno.readTextFileSync("/etc/letsencrypt/live/ddln.app/privkey.pem"),
|
key: Deno.readTextFileSync(keyFile),
|
||||||
}, handler);
|
}, handler);
|
||||||
|
|
||||||
await devServerWatchFiles();
|
await devServerWatchFiles();
|
||||||
|
|||||||
BIN
deno_bootstrap/ddln_bootstrap
Executable file
BIN
deno_bootstrap/ddln_bootstrap
Executable file
Binary file not shown.
121
src/App.ts
121
src/App.ts
@@ -13,12 +13,11 @@ type PeerID = string;
|
|||||||
class Post {
|
class Post {
|
||||||
post_timestamp: Date;
|
post_timestamp: Date;
|
||||||
post_id: string;
|
post_id: string;
|
||||||
|
reply_to_id: string|null;
|
||||||
author: string;
|
author: string;
|
||||||
author_id: string;
|
author_id: string;
|
||||||
text: string;
|
text: string;
|
||||||
image_data: ArrayBuffer | null;
|
image_data: ArrayBuffer | null;
|
||||||
|
|
||||||
|
|
||||||
importedFrom: "twitter" | null;
|
importedFrom: "twitter" | null;
|
||||||
importSource: any;
|
importSource: any;
|
||||||
|
|
||||||
@@ -29,7 +28,8 @@ class Post {
|
|||||||
post_timestamp: Date,
|
post_timestamp: Date,
|
||||||
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) {
|
||||||
|
|
||||||
this.post_timestamp = post_timestamp;
|
this.post_timestamp = post_timestamp;
|
||||||
this.post_id = generateID();
|
this.post_id = generateID();
|
||||||
@@ -41,6 +41,7 @@ class Post {
|
|||||||
|
|
||||||
this.importedFrom = importedFrom;
|
this.importedFrom = importedFrom;
|
||||||
this.importSource = importSource;
|
this.importSource = importSource;
|
||||||
|
this.reply_to_id = reply_to_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +119,7 @@ export class App {
|
|||||||
peername: string = '';
|
peername: string = '';
|
||||||
userID: string = '';
|
userID: string = '';
|
||||||
peerID: string = '';
|
peerID: string = '';
|
||||||
|
replyToID: string|null = null;
|
||||||
following: Set<string> = new Set();
|
following: Set<string> = new Set();
|
||||||
posts: StoragePost[] = [];
|
posts: StoragePost[] = [];
|
||||||
isHeadless: boolean = false;
|
isHeadless: boolean = false;
|
||||||
@@ -342,23 +344,16 @@ export class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
// return posts;
|
|
||||||
|
|
||||||
// return postIDs;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.peerManager.registerRPC('sendPostForUser', async (sendingPeerID: string, userID: string, post: Post) => {
|
this.peerManager.registerRPC('sendPostForUser', async (sendingPeerID: string, userID: string, post: Post) => {
|
||||||
console.log.apply(null, log(`[app] sendPostForUser got post[${logID(post.post_id)}] from peer[${logID(sendingPeerID)}] for user[${logID(userID)}] author[${post.author}] text[${post.text}]`));
|
console.log.apply(null, log(`[app] sendPostForUser got post[${logID(post.post_id)}] from peer[${logID(sendingPeerID)}] for user[${logID(userID)}] author[${post.author}] text[${post.text}]`));
|
||||||
// if (post.text === "image...") {
|
|
||||||
// debugger;
|
|
||||||
// }
|
|
||||||
|
|
||||||
let peerData = this.statusBar.getPeerData(sendingPeerID);
|
let peerData = this.statusBar.getPeerData(sendingPeerID);
|
||||||
if (peerData) {
|
if (peerData) {
|
||||||
this.statusBar.updatePeerMessage(sendingPeerID, `⬇️${logID(userID)} ${peerData.havePostCount}/${peerData.neededPostCount}}`);
|
this.statusBar.updatePeerMessage(sendingPeerID, `⬇️${logID(userID)} ${peerData.havePostCount}/${peerData.neededPostCount}}`);
|
||||||
}
|
}
|
||||||
await this.sync.writePostForUser(userID, post);
|
await this.sync.writePostForUser(userID, post);
|
||||||
// if (userID === this.userID) {
|
|
||||||
|
|
||||||
if (peerData) {
|
if (peerData) {
|
||||||
peerData.havePostCount++
|
peerData.havePostCount++
|
||||||
@@ -372,14 +367,13 @@ export class App {
|
|||||||
this.renderTimer = setTimeout(() => { this.render() }, 1000);
|
this.renderTimer = setTimeout(() => { this.render() }, 1000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.statusBar.setMessageHTML("Connecting to ddln network...")
|
this.statusBar.setMessageHTML("Connecting to ddln...")
|
||||||
await this.peerManager.connect();
|
await this.peerManager.connect();
|
||||||
console.log.apply(null, log("*************** after peerManager.connect"));;
|
console.log.apply(null, log("*************** after peerManager.connect"));;
|
||||||
this.statusBar.setMessageHTML("Connected to ddln network...")
|
this.statusBar.setMessageHTML("Connected to ddln.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -729,7 +723,7 @@ export class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNewPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4") {
|
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) {
|
||||||
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;
|
||||||
@@ -737,14 +731,14 @@ export class App {
|
|||||||
|
|
||||||
if (mediaData &&
|
if (mediaData &&
|
||||||
(mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
|
(mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
|
||||||
(mediaData as ArrayBuffer).byteLength > 500 * 1024) {
|
(mediaData as ArrayBuffer).byteLength > 256 * 1024) {
|
||||||
let compressedImage = await this.compressImage(mediaData as ArrayBuffer, mimeType, 0.9);
|
let compressedImage = await this.compressImage(mediaData as ArrayBuffer, mimeType, 0.9);
|
||||||
if (compressedImage) {
|
if (compressedImage) {
|
||||||
mediaData = compressedImage as ArrayBuffer;
|
mediaData = compressedImage as ArrayBuffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let post = new Post(this.username, userID, postText, new Date(), mediaData);
|
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID);
|
||||||
// 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);
|
||||||
@@ -990,8 +984,7 @@ export class App {
|
|||||||
|
|
||||||
let composeButton = this.div('compose-button');
|
let composeButton = this.div('compose-button');
|
||||||
composeButton.addEventListener('click', e => {
|
composeButton.addEventListener('click', e => {
|
||||||
document.getElementById('compose')!.style.display = 'block';
|
this.enterCompose();
|
||||||
document.getElementById('textarea_post')?.focus();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -999,11 +992,13 @@ 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.createNewPost(this.userID, 'image...', buffer, file.type);
|
await this.createPost(this.userID, 'image...', buffer, file.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = '';
|
||||||
|
|
||||||
|
this.exitCompose();
|
||||||
});
|
});
|
||||||
|
|
||||||
let filePickerLabel = document.getElementById('file-input-label');
|
let filePickerLabel = document.getElementById('file-input-label');
|
||||||
@@ -1040,25 +1035,28 @@ export class App {
|
|||||||
|
|
||||||
// clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() });
|
// clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() });
|
||||||
|
|
||||||
|
let cancelPostButton = document.getElementById("button_cancel_post") as HTMLElement;
|
||||||
let postButton = document.getElementById("button_post") as HTMLButtonElement;
|
|
||||||
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
|
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
|
||||||
|
let postButton = document.getElementById("button_post") as HTMLButtonElement;
|
||||||
|
|
||||||
if (!(postButton && postText)) {
|
if (!(cancelPostButton && postButton && postText)) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelPostButton.addEventListener('click', (e) => {
|
||||||
|
this.exitCompose();
|
||||||
|
});
|
||||||
|
|
||||||
postText.addEventListener('paste', async (e) => {
|
postText.addEventListener('paste', async (e) => {
|
||||||
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.createNewPost(this.userID, 'image...', buffer, file.type as any);
|
await this.createPost(this.userID, 'image...', buffer, file.type as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
postButton.addEventListener("click", () => {
|
postButton.addEventListener("click", () => {
|
||||||
this.createNewPost(userID, postText.value);
|
this.createPost(userID, postText.value, undefined, undefined, this.replyToID);
|
||||||
postText.value = "";
|
this.exitCompose();
|
||||||
document.getElementById('compose')!.style.display = 'none';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// updateApp.addEventListener("click", () => {
|
// updateApp.addEventListener("click", () => {
|
||||||
@@ -1072,6 +1070,31 @@ export class App {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Change this all to a template so we're not toggling state in this crazy way!
|
||||||
|
enterCompose(replyToID:string|null=null) {
|
||||||
|
if (replyToID) {
|
||||||
|
this.renderComposeReplyArea(replyToID);
|
||||||
|
document.getElementById("compose-reply-area")!.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
replyToID = replyToID;
|
||||||
|
|
||||||
|
document.getElementById('compose')!.style.display = 'block';
|
||||||
|
document.getElementById('textarea_post')?.focus();
|
||||||
|
document.getElementById('compose-dimmer')?.classList.add("compose-dimmer-dimmed");
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCompose() {
|
||||||
|
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
|
||||||
|
postText.value = "";
|
||||||
|
document.getElementById('compose')!.style.display = 'none';
|
||||||
|
document.getElementById('compose-dimmer')?.classList.remove("compose-dimmer-dimmed");
|
||||||
|
document.getElementById("compose-reply-area")!.style.display = "none";
|
||||||
|
document.body.classList.remove("no-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
async getPostsForFeed() {
|
async getPostsForFeed() {
|
||||||
|
|
||||||
// get N posts from each user and sort them by date.
|
// get N posts from each user and sort them by date.
|
||||||
@@ -1486,6 +1509,12 @@ export class App {
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderComposeReplyArea(replyToID:string) {
|
||||||
|
let composeReplyArea = document.getElementById('compose-reply-area') as HTMLElement;
|
||||||
|
composeReplyArea.innerText = replyToID;
|
||||||
|
composeReplyArea.classList.add("show");
|
||||||
|
}
|
||||||
|
|
||||||
renderPost(post: Post, first: boolean) {
|
renderPost(post: Post, first: boolean) {
|
||||||
if (!(post.hasOwnProperty("text"))) {
|
if (!(post.hasOwnProperty("text"))) {
|
||||||
throw new Error("Post is malformed!");
|
throw new Error("Post is malformed!");
|
||||||
@@ -1505,6 +1534,16 @@ export class App {
|
|||||||
await navigator.clipboard.writeText(shareUrl)
|
await navigator.clipboard.writeText(shareUrl)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let replyButton = document.createElement('button'); replyButton.innerText = 'reply';
|
||||||
|
replyButton.onclick = async () => {
|
||||||
|
console.log(`replying to post ${post.post_id}`);
|
||||||
|
this.enterCompose(post.post_id);
|
||||||
|
// let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
|
||||||
|
|
||||||
|
// 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;
|
||||||
@@ -1530,11 +1569,17 @@ export class App {
|
|||||||
<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>
|
||||||
</span>
|
</span>
|
||||||
${ownPost ? `<span id="deleteButton"></span>` : ''}
|
|
||||||
${ownPost ? `<span id="editButton"></span>` : ''}
|
|
||||||
<span id="shareButton"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>${markdown}</div>
|
<div>${markdown}</div>
|
||||||
|
|
||||||
|
<div id="image"></div>
|
||||||
|
|
||||||
|
<span id="replyButton"></span>
|
||||||
|
${ownPost ? `<span id="editButton"></span>` : ''}
|
||||||
|
<span id="shareButton"></span>
|
||||||
|
${ownPost ? `<span id="deleteButton"></span>` : ''}
|
||||||
|
|
||||||
|
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
containerDiv.innerHTML = postTemplate;
|
containerDiv.innerHTML = postTemplate;
|
||||||
@@ -1545,18 +1590,15 @@ export class App {
|
|||||||
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
containerDiv.querySelector('#shareButton')?.appendChild(shareButton);
|
containerDiv.querySelector('#shareButton')?.appendChild(shareButton);
|
||||||
|
containerDiv.querySelector('#replyButton')?.appendChild(replyButton);
|
||||||
|
|
||||||
|
|
||||||
if (!("image_data" in post && post.image_data)) {
|
let hasImage = ("image_data" in post && post.image_data);
|
||||||
// containerDiv.appendChild(timestampDiv);
|
if (hasImage) {
|
||||||
return containerDiv;
|
|
||||||
// return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let image = document.createElement("img");
|
let image = document.createElement("img");
|
||||||
image.title = `${(post.image_data.byteLength / 1024 / 1024).toFixed(2)}MBytes`;
|
|
||||||
|
image.title = `${(post.image_data!.byteLength / 1024 / 1024).toFixed(2)}MBytes`;
|
||||||
// const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' });
|
// const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' });
|
||||||
const blob = new Blob([post.image_data as ArrayBuffer]);
|
const blob = new Blob([post.image_data as ArrayBuffer]);
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -1569,9 +1611,12 @@ export class App {
|
|||||||
image.className = "postImage";
|
image.className = "postImage";
|
||||||
// image.onclick = () => { App.maximizeElement(image) };
|
// image.onclick = () => { App.maximizeElement(image) };
|
||||||
|
|
||||||
containerDiv.appendChild(image);
|
containerDiv.querySelector('#image')?.appendChild(image);
|
||||||
|
}
|
||||||
|
|
||||||
// containerDiv.appendChild(timestampDiv);
|
// containerDiv.appendChild(timestampDiv);
|
||||||
|
|
||||||
|
|
||||||
return containerDiv;
|
return containerDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
import { generateID } from "IDUtils";
|
import { generateID } from "IDUtils";
|
||||||
import { log, logID } from "log";
|
import { log, logID } from "log";
|
||||||
// import { App } from "./App";
|
|
||||||
|
|
||||||
// Use a broadcast channel to only have one peer manager for multiple tabs,
|
// Use a broadcast channel to only have one peer manager for multiple tabs,
|
||||||
// then we won't need to have a session ID as all queries for a peerID will be coming from the same peer manager
|
// then we won't need to have a session ID as all queries for a peerID will be coming from the same peer manager
|
||||||
@@ -22,7 +21,6 @@ export class PeerManager {
|
|||||||
routingTable: Map<string, string>;
|
routingTable: Map<string, string>;
|
||||||
|
|
||||||
peers: Map<string, PeerConnection>;
|
peers: Map<string, PeerConnection>;
|
||||||
// private signaler: Signaler;
|
|
||||||
searchQueryFunctions: Map<string, Function> = new Map();
|
searchQueryFunctions: Map<string, Function> = new Map();
|
||||||
RPC_remote: Map<string, Function> = new Map();
|
RPC_remote: Map<string, Function> = new Map();
|
||||||
rpc: { [key: string]: Function } = {};
|
rpc: { [key: string]: Function } = {};
|
||||||
@@ -46,14 +44,6 @@ export class PeerManager {
|
|||||||
reconnectTimer: number | null = null;
|
reconnectTimer: number | null = null;
|
||||||
peerStateSuperlog: boolean = true;
|
peerStateSuperlog: boolean = true;
|
||||||
|
|
||||||
// async watchdog() {
|
|
||||||
// // Check that we're connected to at least N peers. If not, reconnect to the bootstrap server.
|
|
||||||
|
|
||||||
// if (this.peers.size === 0) {
|
|
||||||
// await this.sendHello2();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal'];
|
animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal'];
|
||||||
adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy'];
|
adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy'];
|
||||||
snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait']
|
snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait']
|
||||||
@@ -108,6 +98,18 @@ export class PeerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onPeerMessageFromPeer(remotePeerID: string, messageJSON: any) {
|
||||||
|
let targetPeer = this.peers.get(messageJSON.to);
|
||||||
|
|
||||||
|
if (!targetPeer) {
|
||||||
|
console.log.apply(null, log("[PeerManager] Coulnd't find peer for onPeerMessageFromPeer:", messageJSON.to));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPeer.send(messageJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onWebsocketMessage(event: MessageEvent) {
|
onWebsocketMessage(event: MessageEvent) {
|
||||||
let messageJSON = event.data;
|
let messageJSON = event.data;
|
||||||
let message: any = null;
|
let message: any = null;
|
||||||
@@ -149,10 +151,11 @@ export class PeerManager {
|
|||||||
if (!peerConnection) {
|
if (!peerConnection) {
|
||||||
let remotePeerID = message.from;
|
let remotePeerID = message.from;
|
||||||
let newPeer = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
|
let newPeer = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
|
||||||
if (this._isBootstrapPeer) {
|
|
||||||
newPeer.setPolite(false);
|
newPeer.setPoliteFromID(remotePeerID, true);
|
||||||
}
|
|
||||||
peerConnection = newPeer;
|
peerConnection = newPeer;
|
||||||
|
|
||||||
this.peers.set(newPeer.remotePeerID, newPeer);
|
this.peers.set(newPeer.remotePeerID, newPeer);
|
||||||
|
|
||||||
this.onConnectRequest(newPeer);
|
this.onConnectRequest(newPeer);
|
||||||
@@ -580,6 +583,12 @@ class PeerConnection {
|
|||||||
this.polite = polite;
|
this.polite = polite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPoliteFromID(peerID: PeerID, remote = false) {
|
||||||
|
let polite = (parseInt(peerID.charAt(peerID.length-1), 16) % 2 == 0) && !remote;
|
||||||
|
|
||||||
|
this.setPolite(polite);
|
||||||
|
}
|
||||||
|
|
||||||
setupDataChannel() {
|
setupDataChannel() {
|
||||||
if (!this.dataChannel) {
|
if (!this.dataChannel) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
@@ -713,6 +722,7 @@ class PeerConnection {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.makingOffer = true;
|
this.makingOffer = true;
|
||||||
|
this.setPoliteFromID(this.peerManager.peerID);
|
||||||
await this.rtcPeer.setLocalDescription();
|
await this.rtcPeer.setLocalDescription();
|
||||||
|
|
||||||
if (!this.rtcPeer.localDescription) {
|
if (!this.rtcPeer.localDescription) {
|
||||||
@@ -801,12 +811,6 @@ class PeerConnection {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
// };
|
|
||||||
// */
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
@@ -884,15 +888,12 @@ class PeerConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
call(functionName: string, args: any) {
|
call(functionName: string, args: any) {
|
||||||
let transactionID = generateID(); // make this faster as we will only ever have a small number of in-flight queries on a peer
|
let transactionID = generateID(); // make this faster as we will only ever have a small number of in-flight queries on a peer
|
||||||
// Think about a timeout here to auto reject it after a while.
|
|
||||||
let promise = new Promise((resolve, reject) => {
|
let promise = new Promise((resolve, reject) => {
|
||||||
this.pendingRPCs.set(transactionID, { resolve, reject, functionName });
|
this.pendingRPCs.set(transactionID, { resolve, reject, functionName });
|
||||||
setTimeout(() => reject(`function:${functionName}[${transactionID}] failed to resolve after 10 seconds.`), 10_000);
|
let timeoutSeconds = 10;
|
||||||
|
setTimeout(() => reject(`function:${functionName}[${transactionID}] failed to resolve after ${timeoutSeconds} seconds.`), timeoutSeconds * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = {
|
let message = {
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ export class Sync {
|
|||||||
'194696a2-d850-4bb0-98f7-47416b3d1662',
|
'194696a2-d850-4bb0-98f7-47416b3d1662',
|
||||||
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
|
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
|
||||||
'dd1d92aa-aa24-4166-a925-94ba072a9048',
|
'dd1d92aa-aa24-4166-a925-94ba072a9048',
|
||||||
'290dbb4f-6ce1-491a-b90d-51d8efcd3d60'
|
'290dbb4f-6ce1-491a-b90d-51d8efcd3d60',
|
||||||
|
'3b32d0eb-94d5-49c4-a43b-0959a9fbb015'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
getFollowing(userID: string): string[] {
|
getFollowing(userID: string): string[] {
|
||||||
|
|||||||
43
src/db.ts
43
src/db.ts
@@ -15,7 +15,7 @@ const followingStoreName: string = "following"
|
|||||||
const profileStoreName: string = "profiles"
|
const profileStoreName: string = "profiles"
|
||||||
let keyBase = "dandelion_posts_v1_"
|
let keyBase = "dandelion_posts_v1_"
|
||||||
let key = "";
|
let key = "";
|
||||||
let version = 1;
|
let version = 3;
|
||||||
|
|
||||||
|
|
||||||
interface IDBRequestEvent<T = any> extends Event {
|
interface IDBRequestEvent<T = any> extends Event {
|
||||||
@@ -28,13 +28,22 @@ type DBError = Event & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
async function upgrade_0to1(db: IDBDatabase) {
|
async function upgrade_0to1(db: IDBDatabase, _transaction: IDBTransaction) {
|
||||||
let postsStore = db.createObjectStore(postStoreName, { keyPath: "id", autoIncrement: true });
|
let postsStore = db.createObjectStore(postStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
postsStore.createIndex("datetimeIndex", "post_timestamp", { unique: false });
|
postsStore.createIndex("datetimeIndex", "post_timestamp", { unique: false });
|
||||||
postsStore.createIndex("postIDIndex", "data.post_id", { unique: true });
|
postsStore.createIndex("postIDIndex", "data.post_id", { unique: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function upgrade_1to2(_db: IDBDatabase, _transaction: IDBTransaction) {
|
||||||
|
// Was broken for some clients — index creation moved to upgrade_2to3
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upgrade_2to3(_db: IDBDatabase, transaction: IDBTransaction) {
|
||||||
|
const postsStore = transaction.objectStore(postStoreName);
|
||||||
|
if (!postsStore.indexNames.contains("postReplyIndex")) {
|
||||||
|
postsStore.createIndex("postReplyIndex", "data.reply_to_id", { unique: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// let following = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
|
// let following = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
|
||||||
|
|
||||||
@@ -51,13 +60,13 @@ async function upgrade_0to1(db: IDBDatabase) {
|
|||||||
|
|
||||||
// let tombstones = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
|
// let tombstones = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
|
||||||
|
|
||||||
async function upgrade_1to2(db: IDBDatabase) {
|
// async function upgrade_1to2(db: IDBDatabase) {
|
||||||
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
|
// let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
let profileStore = db.createObjectStore(profileStoreName, { keyPath: "id", autoIncrement: true });
|
// let profileStore = db.createObjectStore(profileStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
let tombstoneStore = db.createObjectStore(tombstoneStoreName, { keyPath: "id", autoIncrement: true });
|
// let tombstoneStore = db.createObjectStore(tombstoneStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
tombstoneStore.createIndex("postIDIndex", "data.post_id", { unique: true });
|
// tombstoneStore.createIndex("postIDIndex", "data.post_id", { unique: true });
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
// async function upgrade_1to2(db: IDBDatabase) {
|
// async function upgrade_1to2(db: IDBDatabase) {
|
||||||
// console.log("Upgrading database from 1 to 2");
|
// console.log("Upgrading database from 1 to 2");
|
||||||
@@ -86,15 +95,15 @@ async function upgrade_1to2(db: IDBDatabase) {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async function upgrade_2to3(db: IDBDatabase) {
|
// async function upgrade_2to3(db: IDBDatabase) {
|
||||||
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
|
// let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
let upgrades = new Map([
|
let upgrades = new Map([
|
||||||
[0, upgrade_0to1],
|
[0, upgrade_0to1],
|
||||||
[1, upgrade_1to2],
|
[1, upgrade_1to2],
|
||||||
[2, upgrade_2to3]
|
[2, upgrade_2to3],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function openDatabase(userID: string): Promise<IDBDatabase> {
|
export function openDatabase(userID: string): Promise<IDBDatabase> {
|
||||||
@@ -110,13 +119,15 @@ export function openDatabase(userID: string): Promise<IDBDatabase> {
|
|||||||
|
|
||||||
request.onupgradeneeded = async (event: IDBVersionChangeEvent) => {
|
request.onupgradeneeded = async (event: IDBVersionChangeEvent) => {
|
||||||
const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
|
const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
|
||||||
|
const transaction: IDBTransaction = (event.target as IDBOpenDBRequest).transaction!;
|
||||||
|
|
||||||
let upgradeFunction = upgrades.get(event.oldVersion);
|
for (let v = event.oldVersion; v < (event.newVersion ?? version); v++) {
|
||||||
|
const upgradeFunction = upgrades.get(v);
|
||||||
if (!upgradeFunction) {
|
if (!upgradeFunction) {
|
||||||
throw new Error(`db: Don't have an upgrade function to go from version ${event.oldVersion} to version ${event.newVersion}`);
|
throw new Error(`db: Don't have an upgrade function to go from version ${v} to version ${v + 1}`);
|
||||||
|
}
|
||||||
|
await upgradeFunction(db, transaction);
|
||||||
}
|
}
|
||||||
// debugger;
|
|
||||||
await upgradeFunction(db);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onsuccess = (event: Event) => {
|
request.onsuccess = (event: Event) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { openDatabase, getData, addData, deleteData, getAllData, getPostForUser
|
|||||||
import { arrayBufferToBase64, compressString } from "dataUtils";
|
import { arrayBufferToBase64, compressString } 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) {
|
constructor(author, author_id, text, post_timestamp, imageData = null, importedFrom = null, importSource = null, reply_to_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;
|
||||||
@@ -14,6 +14,7 @@ class Post {
|
|||||||
this.image_data = imageData;
|
this.image_data = imageData;
|
||||||
this.importedFrom = importedFrom;
|
this.importedFrom = importedFrom;
|
||||||
this.importSource = importSource;
|
this.importSource = importSource;
|
||||||
|
this.reply_to_id = reply_to_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class StatusBar {
|
class StatusBar {
|
||||||
@@ -68,6 +69,7 @@ export class App {
|
|||||||
this.peername = '';
|
this.peername = '';
|
||||||
this.userID = '';
|
this.userID = '';
|
||||||
this.peerID = '';
|
this.peerID = '';
|
||||||
|
this.replyToID = null;
|
||||||
this.following = new Set();
|
this.following = new Set();
|
||||||
this.posts = [];
|
this.posts = [];
|
||||||
this.isHeadless = false;
|
this.isHeadless = false;
|
||||||
@@ -252,20 +254,14 @@ export class App {
|
|||||||
await this.peerManager?.rpc.sendPostForUser(requestingPeerID, this.peerID, userID, post);
|
await this.peerManager?.rpc.sendPostForUser(requestingPeerID, this.peerID, userID, post);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
// return posts;
|
|
||||||
// return postIDs;
|
|
||||||
});
|
});
|
||||||
this.peerManager.registerRPC('sendPostForUser', async (sendingPeerID, userID, post) => {
|
this.peerManager.registerRPC('sendPostForUser', async (sendingPeerID, userID, post) => {
|
||||||
console.log.apply(null, log(`[app] sendPostForUser got post[${logID(post.post_id)}] from peer[${logID(sendingPeerID)}] for user[${logID(userID)}] author[${post.author}] text[${post.text}]`));
|
console.log.apply(null, log(`[app] sendPostForUser got post[${logID(post.post_id)}] from peer[${logID(sendingPeerID)}] for user[${logID(userID)}] author[${post.author}] text[${post.text}]`));
|
||||||
// if (post.text === "image...") {
|
|
||||||
// debugger;
|
|
||||||
// }
|
|
||||||
let peerData = this.statusBar.getPeerData(sendingPeerID);
|
let peerData = this.statusBar.getPeerData(sendingPeerID);
|
||||||
if (peerData) {
|
if (peerData) {
|
||||||
this.statusBar.updatePeerMessage(sendingPeerID, `⬇️${logID(userID)} ${peerData.havePostCount}/${peerData.neededPostCount}}`);
|
this.statusBar.updatePeerMessage(sendingPeerID, `⬇️${logID(userID)} ${peerData.havePostCount}/${peerData.neededPostCount}}`);
|
||||||
}
|
}
|
||||||
await this.sync.writePostForUser(userID, post);
|
await this.sync.writePostForUser(userID, post);
|
||||||
// if (userID === this.userID) {
|
|
||||||
if (peerData) {
|
if (peerData) {
|
||||||
peerData.havePostCount++;
|
peerData.havePostCount++;
|
||||||
this.statusBar.updatePeerMessage(sendingPeerID, `⬇️${logID(userID)} ${peerData.havePostCount}/${peerData.neededPostCount}}`);
|
this.statusBar.updatePeerMessage(sendingPeerID, `⬇️${logID(userID)} ${peerData.havePostCount}/${peerData.neededPostCount}}`);
|
||||||
@@ -275,13 +271,12 @@ export class App {
|
|||||||
}
|
}
|
||||||
this.renderTimer = setTimeout(() => { this.render(); }, 1000);
|
this.renderTimer = setTimeout(() => { this.render(); }, 1000);
|
||||||
return true;
|
return true;
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
this.statusBar.setMessageHTML("Connecting to ddln network...");
|
this.statusBar.setMessageHTML("Connecting to ddln...");
|
||||||
await this.peerManager.connect();
|
await this.peerManager.connect();
|
||||||
console.log.apply(null, log("*************** after peerManager.connect"));
|
console.log.apply(null, log("*************** after peerManager.connect"));
|
||||||
;
|
;
|
||||||
this.statusBar.setMessageHTML("Connected to ddln network...");
|
this.statusBar.setMessageHTML("Connected to ddln.");
|
||||||
if (this.isBootstrapPeer) {
|
if (this.isBootstrapPeer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -530,20 +525,20 @@ export class App {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async createNewPost(userID, postText, mediaData, mimeType) {
|
async createPost(userID, postText, mediaData, mimeType, replyToID = 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;
|
||||||
}
|
}
|
||||||
if (mediaData &&
|
if (mediaData &&
|
||||||
(mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
|
(mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
|
||||||
mediaData.byteLength > 500 * 1024) {
|
mediaData.byteLength > 256 * 1024) {
|
||||||
let compressedImage = await this.compressImage(mediaData, mimeType, 0.9);
|
let compressedImage = await this.compressImage(mediaData, mimeType, 0.9);
|
||||||
if (compressedImage) {
|
if (compressedImage) {
|
||||||
mediaData = compressedImage;
|
mediaData = compressedImage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let post = new Post(this.username, userID, postText, new Date(), mediaData);
|
let post = new Post(this.username, userID, postText, new Date(), mediaData, null, null, replyToID);
|
||||||
// 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);
|
||||||
@@ -736,17 +731,17 @@ export class App {
|
|||||||
});
|
});
|
||||||
let composeButton = this.div('compose-button');
|
let composeButton = this.div('compose-button');
|
||||||
composeButton.addEventListener('click', e => {
|
composeButton.addEventListener('click', e => {
|
||||||
document.getElementById('compose').style.display = 'block';
|
this.enterCompose();
|
||||||
document.getElementById('textarea_post')?.focus();
|
|
||||||
});
|
});
|
||||||
let filePicker = document.getElementById('file-input');
|
let filePicker = document.getElementById('file-input');
|
||||||
filePicker?.addEventListener('change', async (event) => {
|
filePicker?.addEventListener('change', async (event) => {
|
||||||
for (let file of filePicker.files) {
|
for (let file of filePicker.files) {
|
||||||
let buffer = await file.arrayBuffer();
|
let buffer = await file.arrayBuffer();
|
||||||
await this.createNewPost(this.userID, 'image...', buffer, file.type);
|
await this.createPost(this.userID, 'image...', buffer, file.type);
|
||||||
}
|
}
|
||||||
// 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 = '';
|
||||||
|
this.exitCompose();
|
||||||
});
|
});
|
||||||
let filePickerLabel = document.getElementById('file-input-label');
|
let filePickerLabel = document.getElementById('file-input-label');
|
||||||
filePickerLabel?.addEventListener('click', () => {
|
filePickerLabel?.addEventListener('click', () => {
|
||||||
@@ -773,21 +768,24 @@ export class App {
|
|||||||
// this.render();
|
// this.render();
|
||||||
// });
|
// });
|
||||||
// clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() });
|
// clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() });
|
||||||
let postButton = document.getElementById("button_post");
|
let cancelPostButton = document.getElementById("button_cancel_post");
|
||||||
let postText = document.getElementById("textarea_post");
|
let postText = document.getElementById("textarea_post");
|
||||||
if (!(postButton && postText)) {
|
let postButton = document.getElementById("button_post");
|
||||||
|
if (!(cancelPostButton && postButton && postText)) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
cancelPostButton.addEventListener('click', (e) => {
|
||||||
|
this.exitCompose();
|
||||||
|
});
|
||||||
postText.addEventListener('paste', async (e) => {
|
postText.addEventListener('paste', async (e) => {
|
||||||
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.createNewPost(this.userID, 'image...', buffer, file.type);
|
await this.createPost(this.userID, 'image...', buffer, file.type);
|
||||||
});
|
});
|
||||||
postButton.addEventListener("click", () => {
|
postButton.addEventListener("click", () => {
|
||||||
this.createNewPost(userID, postText.value);
|
this.createPost(userID, postText.value, undefined, undefined, this.replyToID);
|
||||||
postText.value = "";
|
this.exitCompose();
|
||||||
document.getElementById('compose').style.display = 'none';
|
|
||||||
});
|
});
|
||||||
// updateApp.addEventListener("click", () => {
|
// updateApp.addEventListener("click", () => {
|
||||||
// registration?.active?.postMessage({ type: "update_app" });
|
// registration?.active?.postMessage({ type: "update_app" });
|
||||||
@@ -796,6 +794,26 @@ export class App {
|
|||||||
// this.showInfo()
|
// this.showInfo()
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
// Change this all to a template so we're not toggling state in this crazy way!
|
||||||
|
enterCompose(replyToID = null) {
|
||||||
|
if (replyToID) {
|
||||||
|
this.renderComposeReplyArea(replyToID);
|
||||||
|
document.getElementById("compose-reply-area").style.display = "block";
|
||||||
|
}
|
||||||
|
replyToID = replyToID;
|
||||||
|
document.getElementById('compose').style.display = 'block';
|
||||||
|
document.getElementById('textarea_post')?.focus();
|
||||||
|
document.getElementById('compose-dimmer')?.classList.add("compose-dimmer-dimmed");
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
}
|
||||||
|
exitCompose() {
|
||||||
|
let postText = document.getElementById("textarea_post");
|
||||||
|
postText.value = "";
|
||||||
|
document.getElementById('compose').style.display = 'none';
|
||||||
|
document.getElementById('compose-dimmer')?.classList.remove("compose-dimmer-dimmed");
|
||||||
|
document.getElementById("compose-reply-area").style.display = "none";
|
||||||
|
document.body.classList.remove("no-scroll");
|
||||||
|
}
|
||||||
async getPostsForFeed() {
|
async getPostsForFeed() {
|
||||||
// get N posts from each user and sort them by date.
|
// get N posts from each user and sort them by date.
|
||||||
// This isn't really going to work very well.
|
// This isn't really going to work very well.
|
||||||
@@ -1090,6 +1108,11 @@ export class App {
|
|||||||
deleteData(userID, postID);
|
deleteData(userID, postID);
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
renderComposeReplyArea(replyToID) {
|
||||||
|
let composeReplyArea = document.getElementById('compose-reply-area');
|
||||||
|
composeReplyArea.innerText = replyToID;
|
||||||
|
composeReplyArea.classList.add("show");
|
||||||
|
}
|
||||||
renderPost(post, first) {
|
renderPost(post, first) {
|
||||||
if (!(post.hasOwnProperty("text"))) {
|
if (!(post.hasOwnProperty("text"))) {
|
||||||
throw new Error("Post is malformed!");
|
throw new Error("Post is malformed!");
|
||||||
@@ -1106,6 +1129,14 @@ export class App {
|
|||||||
let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
|
let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
|
||||||
await navigator.clipboard.writeText(shareUrl);
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
};
|
};
|
||||||
|
let replyButton = document.createElement('button');
|
||||||
|
replyButton.innerText = 'reply';
|
||||||
|
replyButton.onclick = async () => {
|
||||||
|
console.log(`replying to post ${post.post_id}`);
|
||||||
|
this.enterCompose(post.post_id);
|
||||||
|
// let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
|
||||||
|
// 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;
|
||||||
if (this.markedAvailable) {
|
if (this.markedAvailable) {
|
||||||
@@ -1124,11 +1155,17 @@ export class App {
|
|||||||
<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>
|
||||||
</span>
|
</span>
|
||||||
${ownPost ? `<span id="deleteButton"></span>` : ''}
|
|
||||||
${ownPost ? `<span id="editButton"></span>` : ''}
|
|
||||||
<span id="shareButton"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>${markdown}</div>
|
<div>${markdown}</div>
|
||||||
|
|
||||||
|
<div id="image"></div>
|
||||||
|
|
||||||
|
<span id="replyButton"></span>
|
||||||
|
${ownPost ? `<span id="editButton"></span>` : ''}
|
||||||
|
<span id="shareButton"></span>
|
||||||
|
${ownPost ? `<span id="deleteButton"></span>` : ''}
|
||||||
|
|
||||||
|
|
||||||
</div>`;
|
</div>`;
|
||||||
containerDiv.innerHTML = postTemplate;
|
containerDiv.innerHTML = postTemplate;
|
||||||
if (ownPost) {
|
if (ownPost) {
|
||||||
@@ -1136,11 +1173,9 @@ export class App {
|
|||||||
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
// containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
||||||
}
|
}
|
||||||
containerDiv.querySelector('#shareButton')?.appendChild(shareButton);
|
containerDiv.querySelector('#shareButton')?.appendChild(shareButton);
|
||||||
if (!("image_data" in post && post.image_data)) {
|
containerDiv.querySelector('#replyButton')?.appendChild(replyButton);
|
||||||
// containerDiv.appendChild(timestampDiv);
|
let hasImage = ("image_data" in post && post.image_data);
|
||||||
return containerDiv;
|
if (hasImage) {
|
||||||
// return null;
|
|
||||||
}
|
|
||||||
let image = document.createElement("img");
|
let image = document.createElement("img");
|
||||||
image.title = `${(post.image_data.byteLength / 1024 / 1024).toFixed(2)}MBytes`;
|
image.title = `${(post.image_data.byteLength / 1024 / 1024).toFixed(2)}MBytes`;
|
||||||
// const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' });
|
// const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' });
|
||||||
@@ -1153,7 +1188,8 @@ export class App {
|
|||||||
// image.src = image.src = "data:image/png;base64," + post.image;
|
// image.src = image.src = "data:image/png;base64," + post.image;
|
||||||
image.className = "postImage";
|
image.className = "postImage";
|
||||||
// image.onclick = () => { App.maximizeElement(image) };
|
// image.onclick = () => { App.maximizeElement(image) };
|
||||||
containerDiv.appendChild(image);
|
containerDiv.querySelector('#image')?.appendChild(image);
|
||||||
|
}
|
||||||
// containerDiv.appendChild(timestampDiv);
|
// containerDiv.appendChild(timestampDiv);
|
||||||
return containerDiv;
|
return containerDiv;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -51,6 +51,14 @@ export class PeerManager {
|
|||||||
this.messageSuperlog && console.log.apply(null, log("<-signaler:", message));
|
this.messageSuperlog && console.log.apply(null, log("<-signaler:", message));
|
||||||
this.websocket.send(messageJSON);
|
this.websocket.send(messageJSON);
|
||||||
}
|
}
|
||||||
|
onPeerMessageFromPeer(remotePeerID, messageJSON) {
|
||||||
|
let targetPeer = this.peers.get(messageJSON.to);
|
||||||
|
if (!targetPeer) {
|
||||||
|
console.log.apply(null, log("[PeerManager] Coulnd't find peer for onPeerMessageFromPeer:", messageJSON.to));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetPeer.send(messageJSON);
|
||||||
|
}
|
||||||
onWebsocketMessage(event) {
|
onWebsocketMessage(event) {
|
||||||
let messageJSON = event.data;
|
let messageJSON = event.data;
|
||||||
let message = null;
|
let message = null;
|
||||||
@@ -82,9 +90,7 @@ export class PeerManager {
|
|||||||
if (!peerConnection) {
|
if (!peerConnection) {
|
||||||
let remotePeerID = message.from;
|
let remotePeerID = message.from;
|
||||||
let newPeer = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
|
let newPeer = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
|
||||||
if (this._isBootstrapPeer) {
|
newPeer.setPoliteFromID(remotePeerID, true);
|
||||||
newPeer.setPolite(false);
|
|
||||||
}
|
|
||||||
peerConnection = newPeer;
|
peerConnection = newPeer;
|
||||||
this.peers.set(newPeer.remotePeerID, newPeer);
|
this.peers.set(newPeer.remotePeerID, newPeer);
|
||||||
this.onConnectRequest(newPeer);
|
this.onConnectRequest(newPeer);
|
||||||
@@ -163,7 +169,6 @@ export class PeerManager {
|
|||||||
// message: { type: "get_posts_for_user", post_ids: postIds, user_id: message.user_id } }
|
// message: { type: "get_posts_for_user", post_ids: postIds, user_id: message.user_id } }
|
||||||
}
|
}
|
||||||
constructor(userID, peerID, isBootstrapPeer) {
|
constructor(userID, peerID, isBootstrapPeer) {
|
||||||
// private signaler: Signaler;
|
|
||||||
this.searchQueryFunctions = new Map();
|
this.searchQueryFunctions = new Map();
|
||||||
this.RPC_remote = new Map();
|
this.RPC_remote = new Map();
|
||||||
this.rpc = {};
|
this.rpc = {};
|
||||||
@@ -182,12 +187,6 @@ export class PeerManager {
|
|||||||
this.watchdogInterval = null;
|
this.watchdogInterval = null;
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
this.peerStateSuperlog = true;
|
this.peerStateSuperlog = true;
|
||||||
// async watchdog() {
|
|
||||||
// // Check that we're connected to at least N peers. If not, reconnect to the bootstrap server.
|
|
||||||
// if (this.peers.size === 0) {
|
|
||||||
// await this.sendHello2();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
this.animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal'];
|
this.animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal'];
|
||||||
this.adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy'];
|
this.adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy'];
|
||||||
this.snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait'];
|
this.snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait'];
|
||||||
@@ -417,6 +416,10 @@ class PeerConnection {
|
|||||||
setPolite(polite) {
|
setPolite(polite) {
|
||||||
this.polite = polite;
|
this.polite = polite;
|
||||||
}
|
}
|
||||||
|
setPoliteFromID(peerID, remote = false) {
|
||||||
|
let polite = (parseInt(peerID.charAt(peerID.length - 1), 16) % 2 == 0) && !remote;
|
||||||
|
this.setPolite(polite);
|
||||||
|
}
|
||||||
setupDataChannel() {
|
setupDataChannel() {
|
||||||
if (!this.dataChannel) {
|
if (!this.dataChannel) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
@@ -513,6 +516,7 @@ class PeerConnection {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.makingOffer = true;
|
this.makingOffer = true;
|
||||||
|
this.setPoliteFromID(this.peerManager.peerID);
|
||||||
await this.rtcPeer.setLocalDescription();
|
await this.rtcPeer.setLocalDescription();
|
||||||
if (!this.rtcPeer.localDescription) {
|
if (!this.rtcPeer.localDescription) {
|
||||||
return;
|
return;
|
||||||
@@ -590,8 +594,6 @@ class PeerConnection {
|
|||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
// };
|
|
||||||
// */
|
|
||||||
}
|
}
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.rtcPeer?.close();
|
this.rtcPeer?.close();
|
||||||
@@ -651,10 +653,10 @@ class PeerConnection {
|
|||||||
}
|
}
|
||||||
call(functionName, args) {
|
call(functionName, args) {
|
||||||
let transactionID = generateID(); // make this faster as we will only ever have a small number of in-flight queries on a peer
|
let transactionID = generateID(); // make this faster as we will only ever have a small number of in-flight queries on a peer
|
||||||
// Think about a timeout here to auto reject it after a while.
|
|
||||||
let promise = new Promise((resolve, reject) => {
|
let promise = new Promise((resolve, reject) => {
|
||||||
this.pendingRPCs.set(transactionID, { resolve, reject, functionName });
|
this.pendingRPCs.set(transactionID, { resolve, reject, functionName });
|
||||||
setTimeout(() => reject(`function:${functionName}[${transactionID}] failed to resolve after 10 seconds.`), 10000);
|
let timeoutSeconds = 10;
|
||||||
|
setTimeout(() => reject(`function:${functionName}[${transactionID}] failed to resolve after ${timeoutSeconds} seconds.`), timeoutSeconds * 1000);
|
||||||
});
|
});
|
||||||
let message = {
|
let message = {
|
||||||
type: "rpc_call",
|
type: "rpc_call",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -45,7 +45,8 @@ export class Sync {
|
|||||||
'194696a2-d850-4bb0-98f7-47416b3d1662',
|
'194696a2-d850-4bb0-98f7-47416b3d1662',
|
||||||
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
|
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
|
||||||
'dd1d92aa-aa24-4166-a925-94ba072a9048',
|
'dd1d92aa-aa24-4166-a925-94ba072a9048',
|
||||||
'290dbb4f-6ce1-491a-b90d-51d8efcd3d60'
|
'290dbb4f-6ce1-491a-b90d-51d8efcd3d60',
|
||||||
|
'3b32d0eb-94d5-49c4-a43b-0959a9fbb015'
|
||||||
]);
|
]);
|
||||||
// async getPostIdsForUserHandler(data: any) {
|
// async getPostIdsForUserHandler(data: any) {
|
||||||
// let message = data.message;
|
// let message = data.message;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
43
static/db.js
43
static/db.js
@@ -10,12 +10,21 @@ const followingStoreName = "following";
|
|||||||
const profileStoreName = "profiles";
|
const profileStoreName = "profiles";
|
||||||
let keyBase = "dandelion_posts_v1_";
|
let keyBase = "dandelion_posts_v1_";
|
||||||
let key = "";
|
let key = "";
|
||||||
let version = 1;
|
let version = 3;
|
||||||
async function upgrade_0to1(db) {
|
async function upgrade_0to1(db, _transaction) {
|
||||||
let postsStore = db.createObjectStore(postStoreName, { keyPath: "id", autoIncrement: true });
|
let postsStore = db.createObjectStore(postStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
postsStore.createIndex("datetimeIndex", "post_timestamp", { unique: false });
|
postsStore.createIndex("datetimeIndex", "post_timestamp", { unique: false });
|
||||||
postsStore.createIndex("postIDIndex", "data.post_id", { unique: true });
|
postsStore.createIndex("postIDIndex", "data.post_id", { unique: true });
|
||||||
}
|
}
|
||||||
|
async function upgrade_1to2(_db, _transaction) {
|
||||||
|
// Was broken for some clients — index creation moved to upgrade_2to3
|
||||||
|
}
|
||||||
|
async function upgrade_2to3(_db, transaction) {
|
||||||
|
const postsStore = transaction.objectStore(postStoreName);
|
||||||
|
if (!postsStore.indexNames.contains("postReplyIndex")) {
|
||||||
|
postsStore.createIndex("postReplyIndex", "data.reply_to_id", { unique: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
// let following = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
|
// let following = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
|
||||||
// let profiles = [
|
// let profiles = [
|
||||||
// {
|
// {
|
||||||
@@ -27,12 +36,12 @@ async function upgrade_0to1(db) {
|
|||||||
// description: "A very nice person who never does anything wrong.",
|
// description: "A very nice person who never does anything wrong.",
|
||||||
// }];
|
// }];
|
||||||
// let tombstones = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
|
// let tombstones = ['b38b623c-c3fa-4351-9cab-50233c99fa4e'];
|
||||||
async function upgrade_1to2(db) {
|
// async function upgrade_1to2(db: IDBDatabase) {
|
||||||
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
|
// let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
let profileStore = db.createObjectStore(profileStoreName, { keyPath: "id", autoIncrement: true });
|
// let profileStore = db.createObjectStore(profileStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
let tombstoneStore = db.createObjectStore(tombstoneStoreName, { keyPath: "id", autoIncrement: true });
|
// let tombstoneStore = db.createObjectStore(tombstoneStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
tombstoneStore.createIndex("postIDIndex", "data.post_id", { unique: true });
|
// tombstoneStore.createIndex("postIDIndex", "data.post_id", { unique: true });
|
||||||
}
|
// }
|
||||||
// async function upgrade_1to2(db: IDBDatabase) {
|
// async function upgrade_1to2(db: IDBDatabase) {
|
||||||
// console.log("Upgrading database from 1 to 2");
|
// console.log("Upgrading database from 1 to 2");
|
||||||
// console.log("Converting all image arraybuffers to Blobs")
|
// console.log("Converting all image arraybuffers to Blobs")
|
||||||
@@ -55,13 +64,13 @@ async function upgrade_1to2(db) {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
async function upgrade_2to3(db) {
|
// async function upgrade_2to3(db: IDBDatabase) {
|
||||||
let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
|
// let followingStore = db.createObjectStore(followingStoreName, { keyPath: "id", autoIncrement: true });
|
||||||
}
|
// }
|
||||||
let upgrades = new Map([
|
let upgrades = new Map([
|
||||||
[0, upgrade_0to1],
|
[0, upgrade_0to1],
|
||||||
[1, upgrade_1to2],
|
[1, upgrade_1to2],
|
||||||
[2, upgrade_2to3]
|
[2, upgrade_2to3],
|
||||||
]);
|
]);
|
||||||
export function openDatabase(userID) {
|
export function openDatabase(userID) {
|
||||||
const dbName = `user_${userID}`;
|
const dbName = `user_${userID}`;
|
||||||
@@ -73,12 +82,14 @@ export function openDatabase(userID) {
|
|||||||
};
|
};
|
||||||
request.onupgradeneeded = async (event) => {
|
request.onupgradeneeded = async (event) => {
|
||||||
const db = event.target.result;
|
const db = event.target.result;
|
||||||
let upgradeFunction = upgrades.get(event.oldVersion);
|
const transaction = event.target.transaction;
|
||||||
|
for (let v = event.oldVersion; v < (event.newVersion ?? version); v++) {
|
||||||
|
const upgradeFunction = upgrades.get(v);
|
||||||
if (!upgradeFunction) {
|
if (!upgradeFunction) {
|
||||||
throw new Error(`db: Don't have an upgrade function to go from version ${event.oldVersion} to version ${event.newVersion}`);
|
throw new Error(`db: Don't have an upgrade function to go from version ${v} to version ${v + 1}`);
|
||||||
|
}
|
||||||
|
await upgradeFunction(db, transaction);
|
||||||
}
|
}
|
||||||
// debugger;
|
|
||||||
await upgradeFunction(db);
|
|
||||||
};
|
};
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
const db = event.target.result;
|
const db = event.target.result;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -111,32 +111,41 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="compose">
|
|
||||||
<div id="buttons">
|
|
||||||
<!-- <button id="button_font1" >font1</button>
|
|
||||||
<button id="button_font2" >font2 </button> -->
|
|
||||||
<!-- <button id="import_tweets">import</button> -->
|
|
||||||
<!-- <button id="clear_posts">clear </button> -->
|
|
||||||
<!-- <button id="update_app">check for updates</button> -->
|
|
||||||
<!-- <button id="toggle_dark">light/dark</button> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea cols="60" rows="6" id="textarea_post"></textarea>
|
|
||||||
<div class="right">
|
|
||||||
<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">
|
|
||||||
|
|
||||||
<!-- <button id="button_add_pic" >🏞️</button> -->
|
|
||||||
<button id="button_post" class="button button-big">post</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <div id="torrent-content"></div> -->
|
<!-- <div id="torrent-content"></div> -->
|
||||||
|
|
||||||
|
|
||||||
<div id="content"></div>
|
<div id="content"></div>
|
||||||
|
|
||||||
<div id="compose-button" class="compose-button emoji-fill">✏️</div>
|
<div id="compose-button" class="compose-button emoji-fill">✏️</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="compose-dimmer" class="compose-dimmer-normal"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="compose">
|
||||||
|
<div id="button_cancel_post" class="link">Cancel</div>
|
||||||
|
|
||||||
|
<div id="compose-reply-area"></div>
|
||||||
|
<textarea cols="60" rows="6" id="textarea_post"></textarea>
|
||||||
|
<div class="compose-footer">
|
||||||
|
<div class="compose-left-buttons">
|
||||||
|
<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">
|
||||||
|
</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>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
140
static/main.css
140
static/main.css
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--main-bg-color: white;
|
--main-bg-color: white;
|
||||||
--border-color: rgb(132, 136, 138);
|
--main-hover-color: rgb(64, 64, 64) --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);
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--main-bg-color: black;
|
--main-bg-color: black;
|
||||||
|
--main-hover-color: rgb(64, 64, 64);
|
||||||
--border-color: rgb(132, 136, 138);
|
--border-color: rgb(132, 136, 138);
|
||||||
--edge-color: rgb(60, 60, 60);
|
--edge-color: rgb(60, 60, 60);
|
||||||
--main-fg-color: rgb(202, 208, 211);
|
--main-fg-color: rgb(202, 208, 211);
|
||||||
@@ -22,6 +23,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* html, body {
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
} */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
color: var(--main-fg-color);
|
color: var(--main-fg-color);
|
||||||
@@ -52,14 +57,21 @@ hr {
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
color: var(--main-fg-color);
|
color: var(--main-fg-color);
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-left: 30px;
|
padding-left: 20px;
|
||||||
padding-right: 30px;
|
padding-right: 20px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
|
|
||||||
|
width: 98%;
|
||||||
|
margin: 1%;
|
||||||
|
height: 270px;
|
||||||
|
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
|
max-height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-container {
|
.flex-container {
|
||||||
@@ -72,17 +84,35 @@ hr {
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
/* Your preferred max width for the content */
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
/* Shorthand for flex-grow, flex-shrink and flex-basis */
|
box-sizing: border-box;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
/* Minimum width the content can shrink to */
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 0 5px var(--edge-color);
|
box-shadow: 0 0 5px var(--edge-color);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
/* Hide horizontal overflow inside the flex container */
|
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-dimmer-normal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 30;
|
||||||
|
background-color: black;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-dimmer-dimmed {
|
||||||
|
background-color: black;
|
||||||
|
opacity: 0.7;
|
||||||
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.embed {
|
.embed {
|
||||||
@@ -104,16 +134,43 @@ hr {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
/* .compose-footer {
|
||||||
text-align: right;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.compose-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#buttons {
|
.compose-left-buttons,
|
||||||
margin-left: 40px;
|
.compose-right-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px; /* Puts exactly 10px of space between buttons in the same group */
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#button_post {
|
.link {
|
||||||
margin-right: 40px;
|
user-select: none;
|
||||||
|
display:inline-block;
|
||||||
|
}
|
||||||
|
.link:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -160,6 +217,7 @@ a {
|
|||||||
|
|
||||||
button,
|
button,
|
||||||
.button {
|
.button {
|
||||||
|
user-select: none;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -175,6 +233,15 @@ button,
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
.button:hover {
|
||||||
|
background-color: var(--main-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#deleteButton {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
.button-big {
|
.button-big {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
}
|
}
|
||||||
@@ -202,8 +269,39 @@ iframe {
|
|||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* #compose {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
} */
|
||||||
|
|
||||||
#compose {
|
#compose {
|
||||||
display: none;
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 5%;
|
||||||
|
left: 50%;
|
||||||
|
width: 88%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
max-height: 85vh;
|
||||||
|
max-width: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#compose-reply-area {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show {
|
||||||
|
display:block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
@@ -260,6 +358,7 @@ iframe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.burger-menu {
|
.burger-menu {
|
||||||
|
user-select: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -269,7 +368,7 @@ iframe {
|
|||||||
top: 0px;
|
top: 0px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 1000;
|
z-index: 20;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--main-fg-color);
|
color: var(--main-fg-color);
|
||||||
@@ -280,14 +379,14 @@ iframe {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 900;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.nav-container {
|
.nav-container {
|
||||||
display: none;
|
display: none;
|
||||||
/* transform: translateX(-100%); */
|
/* transform: translateX(-100%); */
|
||||||
/* z-index: 1000; */
|
z-index:10;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-container.active {
|
.nav-container.active {
|
||||||
@@ -343,6 +442,7 @@ iframe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.compose-button {
|
.compose-button {
|
||||||
|
user-select: none;
|
||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
@@ -366,7 +466,7 @@ iframe {
|
|||||||
align-content: center;
|
align-content: center;
|
||||||
padding-left: 55px;
|
padding-left: 55px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height:12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
@@ -378,6 +478,10 @@ iframe {
|
|||||||
/* font-size: 64px; */
|
/* font-size: 64px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-scroll {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|||||||
Reference in New Issue
Block a user