diff --git a/client/src/components/Home.tsx b/client/src/components/Home.tsx
index 90ecb2a..83a329c 100644
--- a/client/src/components/Home.tsx
+++ b/client/src/components/Home.tsx
@@ -4,8 +4,6 @@ import PostCard from './PostCard/PostCard';
import Header from './Header/Header';
import NewThreadCard from './PostCard/NewThreadCard';
-let filterDifficulty: number = 1;
-
// Define the Event interface
interface Event {
id: string;
@@ -23,47 +21,17 @@ const Home = () => {
useEffect(() => {
relay.on('connect', async () => {
console.log(`connected to ${relay.url}`);
- console.log(Math.floor(Date.now() - (30 * 24 * 60 * 60 * 1000)))
-
- // Pad the ID with leading zeros based on filterDifficulty
- const paddedId = "0".padStart(filterDifficulty + 1, '0');
- console.log(paddedId);
const eventList = await relay.list([
{
- ids: ['00'], //prefix number of leading zeros from filterDifficulty
+ ids: ['0000'],
kinds: [1],
- //until: Date.now(),
limit: 10,
- //since: Math.floor(Date.now() - (30 * 24 * 60 * 60 * 1000)), // 24 hours ago
},
]);
- // const socket = new WebSocket('wss://relay.damus.io');
- // socket.onopen = () => {
- // console.log('WebSocket connected');
- // const subscription = [
- // 'REQ',
- // 'POW-TEST',
- // {
- // ids: ["0000"],
- // limit: 10,
- // },
- // ];
- // socket.send(JSON.stringify(subscription));
- // };
-
- // let i = 0;
- // let start = Date.now();
- // socket.onmessage = event => {
- // console.log(event.data);
- // };
- // socket.onerror = error => {
- // console.error('WebSocket error:', error);
- // };
// Assuming eventList is of type Event[]
setEvents(eventList);
- console.log(eventList);
});
relay.on('error', () => {
@@ -79,7 +47,7 @@ const Home = () => {
{events.map((event, index) => (
-
+
))}
diff --git a/client/src/components/PostCard/PostCard.tsx b/client/src/components/PostCard/PostCard.tsx
index 43e4446..5fe46ca 100644
--- a/client/src/components/PostCard/PostCard.tsx
+++ b/client/src/components/PostCard/PostCard.tsx
@@ -1,16 +1,11 @@
import CardContainer from './CardContainer';
-interface PostCardProps {
- content: string;
- time: Date;
-}
+const PostCard = ({ content }: { content: string }) => {
-const PostCard = ({ content, time }: PostCardProps) => {
return (
<>
- {time.toString()}
{content}
diff --git a/client/src/constants/settings.ts b/client/src/constants/settings.ts
deleted file mode 100644
index ada2d67..0000000
--- a/client/src/constants/settings.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-let filterDifficulty: number = 0;
-let difficulty: number = 16;
-let timeout: number = 5;
-
diff --git a/client/src/func/events.ts b/client/src/func/events.ts
deleted file mode 100644
index f410469..0000000
--- a/client/src/func/events.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import {Event} from 'nostr-tools';
-import {zeroLeadingBitsCount} from '../utils/crypto';
-
-export const isEvent =
(evt?: T): evt is T => evt !== undefined;
-export const isMention = ([tag, , , marker]: string[]) => tag === 'e' && marker === 'mention';
-export const isPTag = ([tag]: string[]) => tag === 'p';
-export const hasEventTag = (tag: string[]) => tag[0] === 'e';
-export const isNotNonceTag = ([tag]: string[]) => tag !== 'nonce';
-
-/**
- * validate proof-of-work of a nostr event per nip-13.
- * the validation always requires difficulty commitment in the nonce tag.
- *
- * @param {EventObj} evt event to validate
- * TODO: @param {number} targetDifficulty target proof-of-work difficulty
- */
-export const validatePow = (evt: Event) => {
- const tag = evt.tags.find(tag => tag[0] === 'nonce');
- if (!tag) {
- return false;
- }
- const difficultyCommitment = Number(tag[2]);
- if (!difficultyCommitment || Number.isNaN(difficultyCommitment)) {
- return false;
- }
- return zeroLeadingBitsCount(evt.id) >= difficultyCommitment;
-}
-
-export const sortByCreatedAt = (evt1: Event, evt2: Event) => {
- if (evt1.created_at === evt2.created_at) {
- // console.log('TODO: OMG exactly at the same time, figure out how to sort then', evt1, evt2);
- }
- return evt1.created_at > evt2.created_at ? -1 : 1;
-};
-
-export const sortEventCreatedAt = (created_at: number) => (
- {created_at: a}: Event,
- {created_at: b}: Event,
-) => (
- Math.abs(a - created_at) < Math.abs(b - created_at) ? -1 : 1
-);
-
-const isReply = ([tag, , , marker]: string[]) => tag === 'e' && marker !== 'mention';
-
-/**
- * find reply-to ID according to nip-10, find marked reply or root tag or
- * fallback to positional (last) e tag or return null
- * @param {event} evt
- * @returns replyToID | null
- */
-export const getReplyTo = (evt: Event): string | null => {
- const eventTags = evt.tags.filter(isReply);
- const withReplyMarker = eventTags.filter(([, , , marker]) => marker === 'reply');
- if (withReplyMarker.length === 1) {
- return withReplyMarker[0][1];
- }
- const withRootMarker = eventTags.filter(([, , , marker]) => marker === 'root');
- if (withReplyMarker.length === 0 && withRootMarker.length === 1) {
- return withRootMarker[0][1];
- }
- // fallback to deprecated positional 'e' tags (nip-10)
- const lastTag = eventTags.at(-1);
- return lastTag ? lastTag[1] : null;
-};
diff --git a/client/src/func/main.ts b/client/src/func/main.ts
deleted file mode 100644
index 3b7ad7f..0000000
--- a/client/src/func/main.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-import {Event, nip19} from 'nostr-tools';
-import {zeroLeadingBitsCount} from './utils/crypto';
-import {elem} from './utils/dom';
-import {bounce} from './utils/time';
-import {isWssUrl} from './utils/url';
-import {closeSettingsView, config, toggleSettingsView} from './settings';
-import {subGlobalFeed, subEventID, subNote} from './subscriptions'
-import {getReplyTo, hasEventTag, isEvent, isMention, sortByCreatedAt, sortEventCreatedAt} from './events';
-import {clearView, getViewContent, getViewElem, getViewOptions, setViewElem, view} from './view';
-import {EventWithNip19, EventWithNip19AndReplyTo, textNoteList, replyList} from './notes';
-import {createContact, createTextNote, renderEventDetails, renderRecommendServer, renderUpdateContact} from './ui';
-
-// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
-
-type EventRelayMap = {
- [eventId: string]: string[];
-};
-const eventRelayMap: EventRelayMap = {}; // eventId: [relay1, relay2]
-
-const renderNote = (
- evt: EventWithNip19,
- i: number,
- sortedFeeds: EventWithNip19[],
-) => {
- if (getViewElem(evt.id)) { // note already in view
- return;
- }
- const article = createTextNote(evt, eventRelayMap[evt.id][0]);
- if (i === 0) {
- getViewContent().append(article);
- } else {
- getViewElem(sortedFeeds[i - 1].id).before(article);
- }
- setViewElem(evt.id, article);
-};
-
-const hasEnoughPOW = (
- [tag, , commitment]: string[],
- eventId: string
-) => {
- return tag === 'nonce' && Number(commitment) >= config.filterDifficulty && zeroLeadingBitsCount(eventId) >= config.filterDifficulty;
-};
-
-const renderFeed = bounce(() => {
- const view = getViewOptions();
- switch (view.type) {
- case 'note':
- textNoteList
- .concat(replyList) // search id in notes and replies
- .filter(note => note.id === view.id)
- .forEach(renderNote);
- break;
- case 'feed':
- const now = Math.floor(Date.now() * 0.001);
- textNoteList
- .filter(note => {
- // dont render notes from the future
- if (note.created_at > now) return false;
- // if difficulty filter is configured dont render notes with too little pow
- return !config.filterDifficulty || note.tags.some(tag => hasEnoughPOW(tag, note.id))
- })
- .sort(sortByCreatedAt)
- .reverse()
- .forEach(renderNote);
- break;
- }
-}, 17); // (16.666 rounded, an arbitrary value to limit updates to max 60x per s)
-
-const renderReply = (evt: EventWithNip19AndReplyTo) => {
- const parent = getViewElem(evt.replyTo);
- if (!parent || getViewElem(evt.id)) {
- return;
- }
- let replyContainer = parent.querySelector('.mbox-replies');
- if (!replyContainer) {
- replyContainer = elem('div', {className: 'mbox-replies'});
- parent.append(replyContainer);
- parent.classList.add('mbox-has-replies');
- }
- const reply = createTextNote(evt, eventRelayMap[evt.id][0]);
- replyContainer.append(reply);
- setViewElem(evt.id, reply);
-};
-
-const handleReply = (evt: EventWithNip19, relay: string) => {
- if (
- getViewElem(evt.id) // already rendered probably received from another relay
- || evt.tags.some(isMention) // ignore mentions for now
- ) {
- return;
- }
- const replyTo = getReplyTo(evt);
- if (!replyTo) {
- return;
- }
- const evtWithReplyTo = {replyTo, ...evt};
- replyList.push(evtWithReplyTo);
- renderReply(evtWithReplyTo);
-};
-
-const handleTextNote = (evt: Event, relay: string) => {
- if (evt.content.startsWith('vmess://') && !evt.content.includes(' ')) {
- console.info('drop VMESS encrypted message');
- return;
- }
- if (eventRelayMap[evt.id]) {
- eventRelayMap[evt.id] = [...(eventRelayMap[evt.id]), relay]; // TODO: remove eventRelayMap and just check for getViewElem?
- } else {
- eventRelayMap[evt.id] = [relay];
- const evtWithNip19 = {
- nip19: {
- note: nip19.noteEncode(evt.id),
- npub: nip19.npubEncode(evt.pubkey),
- },
- ...evt,
- };
- if (evt.tags.some(hasEventTag)) {
- handleReply(evtWithNip19, relay);
- } else {
- textNoteList.push(evtWithNip19);
- }
- }
- if (!getViewElem(evt.id)) {
- renderFeed();
- }
-};
-
-const rerenderFeed = () => {
- clearView();
- renderFeed();
-};
-config.rerenderFeed = rerenderFeed;
-
-const onEvent = (evt: Event, relay: string) => {
- switch (evt.kind) {
- case 0:
- handleTextNote(evt, relay);
- break;
- default:
- // console.log(`TODO: add support for event kind ${evt.kind}`/*, evt*/)
- }
-};
-
-// subscribe and change view
-const route = (path: string) => {
- if (path === '/') {
- subGlobalFeed(onEvent);
- view('/feed', {type: 'feed'});
- return;
- }
- if (path === '/feed') {
- subGlobalFeed(onEvent);
- view('/feed', {type: 'feed'});
- } else if (path.length === 64 && path.match(/^\/[0-9a-z]+$/)) {
- const {type, data} = nip19.decode(path.slice(1));
- if (typeof data !== 'string') {
- console.warn('nip19 ProfilePointer, EventPointer and AddressPointer are not yet supported');
- return;
- }
- switch(type) {
- case 'note':
- subNote(data, onEvent);
- view(path, {type: 'note', id: data});
- break;
- default:
- console.warn(`type ${type} not yet supported`);
- }
- renderFeed();
- } else if (path.length === 65) {
- const eventID = path.slice(1);
- subEventID(eventID, onEventDetails);
- view(path, {type: 'event', id: eventID});
- } else {
- console.warn('no support for ', path);
- }
-};
-
-// onload
-route(location.pathname);
-
-// only push a new entry if there is no history onload
-if (!history.length) {
- history.pushState({}, '', location.pathname);
-}
-
-window.addEventListener('popstate', (event) => {
- route(location.pathname);
-});
diff --git a/client/src/func/relays.ts b/client/src/func/relays.ts
deleted file mode 100644
index a934344..0000000
--- a/client/src/func/relays.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import {Event, Filter, relayInit, Relay, Sub} from 'nostr-tools';
-
-type SubCallback = (
- event: Readonly,
- relay: Readonly,
-) => void;
-
-type Subscribe = {
- cb: SubCallback;
- filter: Filter;
- unsub?: boolean;
-};
-
-const subList: Array = [];
-const currentSubList: Array = [];
-const relayMap = new Map();
-
-export const addRelay = async (url: string) => {
- const relay = relayInit(url);
- relay.on('connect', () => {
- console.info(`connected to ${relay.url}`);
- });
- relay.on('error', () => {
- console.warn(`failed to connect to ${relay.url}`);
- });
- try {
- await relay.connect();
- currentSubList.forEach(({cb, filter}) => subscribe(cb, filter, relay));
- relayMap.set(url, relay);
- } catch {
- console.warn(`could not connect to ${url}`);
- }
-};
-
-export const unsubscribe = (sub: Sub) => {
- sub.unsub();
- subList.splice(subList.indexOf(sub), 1);
-};
-
-const subscribe = (
- cb: SubCallback,
- filter: Filter,
- relay: Relay,
- unsub?: boolean
-) => {
- const sub = relay.sub([filter]);
- subList.push(sub);
- sub.on('event', (event: Event) => {
- cb(event, relay.url);
- });
- if (unsub) {
- sub.on('eose', () => {
- // console.log('eose', relay.url);
- unsubscribe(sub);
- });
- }
- return sub;
-};
-
-export const sub = (obj: Subscribe) => {
- currentSubList.push(obj);
- relayMap.forEach((relay) => subscribe(obj.cb, obj.filter, relay, obj.unsub));
-};
-
-export const subOnce = (
- obj: Subscribe & {relay: string}
-) => {
- const relay = relayMap.get(obj.relay);
- if (relay) {
- const sub = subscribe(obj.cb, obj.filter, relay);
- sub.on('eose', () => {
- // console.log('eose', obj.relay);
- unsubscribe(sub);
- });
- }
-};
-
-export const unsubAll = () => {
- subList.forEach(unsubscribe);
- currentSubList.length = 0;
-};
-
-type PublishCallback = (
- relay: string,
- errorMessage?: string,
-) => void;
-
-export const publish = (
- event: Event,
- cb: PublishCallback,
-) => {
- relayMap.forEach((relay, url) => {
- const pub = relay.publish(event);
- pub.on('ok', () => {
- console.info(`${relay.url} has accepted our event`);
- cb(relay.url);
- });
- pub.on('failed', (reason: any) => {
- console.error(`failed to publish to ${relay.url}: ${reason}`);
- cb(relay.url, reason);
- });
- });
-};
-
-
-addRelay('wss://relay.snort.social');
-addRelay('wss://nostr.bitcoiner.social');
-addRelay('wss://nostr.mom');
-addRelay('wss://relay.nostr.bg');
-addRelay('wss://nos.lol');
-// addRelay('wss://relay.nostr.ch');
diff --git a/client/src/func/settings.ts b/client/src/func/settings.ts
deleted file mode 100644
index a26504c..0000000
--- a/client/src/func/settings.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-import {generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools';
-import {updateElemHeight} from './utils/dom';
-import {powEvent} from './system';
-import {publish} from './relays';
-
-const settingsView = document.querySelector('#settings') as HTMLElement;
-
-export const closeSettingsView = () => settingsView.hidden = true;
-
-export const toggleSettingsView = () => settingsView.hidden = !settingsView.hidden;
-
-let pubkey: string = '';
-
-const loadOrGenerateKeys = () => {
- const storedPubKey = localStorage.getItem('pub_key');
- if (storedPubKey) {
- return storedPubKey;
- }
- const privatekey = generatePrivateKey();
- const pubkey = getPublicKey(privatekey);
- localStorage.setItem('private_key', privatekey);
- localStorage.setItem('pub_key', pubkey);
- return pubkey;
-};
-
-let filterDifficulty: number = 0;
-let difficulty: number = 16;
-let timeout: number = 5;
-let rerenderFeed: (() => void) | undefined;
-
-/**
- * global config object
- * config.pubkey, if not set loaded from localStorage or generate a new key
- */
-export const config = {
- get pubkey() {
- if (!pubkey) {
- pubkey = loadOrGenerateKeys();
- }
- return pubkey;
- },
- set pubkey(value) {
- console.info(`pubkey was set to ${value}`);
- pubkey = value;
- },
- get filterDifficulty() {
- return filterDifficulty;
- },
- get difficulty() {
- return difficulty;
- },
- get timeout() {
- return timeout;
- },
- set rerenderFeed(value: () => void) {
- rerenderFeed = value;
- }
-};
-
-const getNumberFromStorage = (
- item: string,
- fallback: number,
-) => {
- const stored = localStorage.getItem(item);
- if (!stored) {
- return fallback;
- }
- return Number(stored);
-};
-
-// filter difficulty
-const filterDifficultyInput = document.querySelector('#filterDifficulty') as HTMLInputElement;
-const filterDifficultyDisplay = document.querySelector('[data-display="filter_difficulty"]') as HTMLElement;
-filterDifficultyInput.addEventListener('input', (e) => {
- localStorage.setItem('filter_difficulty', filterDifficultyInput.value);
- filterDifficulty = filterDifficultyInput.valueAsNumber;
- filterDifficultyDisplay.textContent = filterDifficultyInput.value;
- rerenderFeed && rerenderFeed();
-});
-filterDifficulty = getNumberFromStorage('filter_difficulty', 0);
-filterDifficultyInput.valueAsNumber = filterDifficulty;
-filterDifficultyDisplay.textContent = filterDifficultyInput.value;
-
-// mining difficulty target
-const miningTargetInput = document.querySelector('#miningTarget') as HTMLInputElement;
-miningTargetInput.addEventListener('input', (e) => {
- localStorage.setItem('mining_target', miningTargetInput.value);
- difficulty = miningTargetInput.valueAsNumber;
-});
-// arbitrary difficulty default, still experimenting.
-difficulty = getNumberFromStorage('mining_target', 16);
-miningTargetInput.valueAsNumber = difficulty;
-
-// mining timeout
-const miningTimeoutInput = document.querySelector('#miningTimeout') as HTMLInputElement;
-miningTimeoutInput.addEventListener('input', (e) => {
- localStorage.setItem('mining_timeout', miningTimeoutInput.value);
- timeout = miningTimeoutInput.valueAsNumber;
-});
-timeout = getNumberFromStorage('mining_timeout', 5);
-miningTimeoutInput.valueAsNumber = timeout;
-
-
-// settings
-const settingsForm = document.querySelector('form[name="settings"]') as HTMLFormElement;
-const privateKeyInput = settingsForm.querySelector('#privatekey') as HTMLInputElement;
-const pubKeyInput = settingsForm.querySelector('#pubkey') as HTMLInputElement;
-const statusMessage = settingsForm.querySelector('#keystatus') as HTMLElement;
-const generateBtn = settingsForm.querySelector('button[name="generate"]') as HTMLButtonElement;
-const importBtn = settingsForm.querySelector('button[name="import"]') as HTMLButtonElement;
-const privateTgl = settingsForm.querySelector('button[name="privatekey-toggle"]') as HTMLButtonElement;
-
-const validKeys = (
- privatekey: string,
- pubkey: string,
-) => {
- try {
- if (getPublicKey(privatekey) === pubkey) {
- statusMessage.hidden = true;
- statusMessage.textContent = 'public-key corresponds to private-key';
- importBtn.removeAttribute('disabled');
- return true;
- } else {
- statusMessage.textContent = 'private-key does not correspond to public-key!'
- }
- } catch (e) {
- statusMessage.textContent = `not a valid private-key: ${e.message || e}`;
- }
- statusMessage.hidden = false;
- importBtn.disabled = true;
- return false;
-};
-
-generateBtn.addEventListener('click', () => {
- const privatekey = generatePrivateKey();
- const pubkey = getPublicKey(privatekey);
- if (validKeys(privatekey, pubkey)) {
- privateKeyInput.value = privatekey;
- pubKeyInput.value = pubkey;
- statusMessage.textContent = 'private-key created!';
- statusMessage.hidden = false;
- }
-});
-
-importBtn.addEventListener('click', () => {
- const privatekey = privateKeyInput.value;
- const pubkeyInput = pubKeyInput.value;
- if (validKeys(privatekey, pubkeyInput)) {
- localStorage.setItem('private_key', privatekey);
- localStorage.setItem('pub_key', pubkeyInput);
- statusMessage.textContent = 'stored private and public key locally!';
- statusMessage.hidden = false;
- config.pubkey = pubkeyInput;
- }
-});
-
-settingsForm.addEventListener('input', () => validKeys(privateKeyInput.value, pubKeyInput.value));
-
-privateKeyInput.addEventListener('paste', (event) => {
- if (pubKeyInput.value || !event.clipboardData) {
- return;
- }
- if (privateKeyInput.value === '' || ( // either privatekey field is empty
- privateKeyInput.selectionStart === 0 // or the whole text is selected and replaced with the clipboard
- && privateKeyInput.selectionEnd === privateKeyInput.value.length
- )) { // only generate the pubkey if no data other than the text from clipboard will be used
- try {
- pubKeyInput.value = getPublicKey(event.clipboardData.getData('text'));
- } catch(err) {} // settings form will call validKeys on input and display the error
- }
-});
-
-privateTgl.addEventListener('click', () => {
- privateKeyInput.type = privateKeyInput.type === 'text' ? 'password' : 'text';
-});
-
-privateKeyInput.value = localStorage.getItem('private_key') || '';
-pubKeyInput.value = localStorage.getItem('pub_key') || '';
-
-// profile
-const profileForm = document.querySelector('form[name="profile"]') as HTMLFormElement;
-const profileSubmit = profileForm.querySelector('button[type="submit"]') as HTMLButtonElement;
-const profileStatus = document.querySelector('#profilestatus') as HTMLElement;
-
-profileForm.addEventListener('input', (e) => {
- if (e.target instanceof HTMLElement) {
- if (e.target?.nodeName === 'TEXTAREA') {
- updateElemHeight(e.target as HTMLTextAreaElement);
- }
- }
- const form = new FormData(profileForm);
- const name = form.get('name');
- const about = form.get('about');
- const picture = form.get('picture');
- profileSubmit.disabled = !(name || about || picture);
-});
-
-profileForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- const form = new FormData(profileForm);
- const newProfile = await powEvent({
- kind: 0,
- pubkey: config.pubkey,
- content: JSON.stringify(Object.fromEntries(form)),
- tags: [],
- created_at: Math.floor(Date.now() * 0.001)
- }, {
- difficulty: config.difficulty,
- statusElem: profileStatus,
- timeout: config.timeout,
- }).catch(console.warn);
- if (!newProfile) {
- profileStatus.textContent = 'publishing profile data canceled';
- profileStatus.hidden = false;
- return;
- }
- const privatekey = localStorage.getItem('private_key');
- if (!privatekey) {
- profileStatus.textContent = 'no private key to sign';
- profileStatus.hidden = false;
- return;
- }
- const sig = signEvent(newProfile, privatekey);
- // TODO: validateEvent
- if (sig) {
- publish({...newProfile, sig}, (relay, error) => {
- if (error) {
- return console.error(error, relay);
- }
- console.info(`publish request sent to ${relay}`);
- profileStatus.textContent = 'profile successfully published';
- profileStatus.hidden = false;
- profileSubmit.disabled = true;
- });
- }
-});
diff --git a/client/src/func/subscriptions.ts b/client/src/func/subscriptions.ts
deleted file mode 100644
index d47777f..0000000
--- a/client/src/func/subscriptions.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import {Event} from 'nostr-tools';
-import {getReplyTo, hasEventTag, isMention, isPTag} from './events';
-import {config} from './settings';
-import {sub, subOnce, unsubAll} from './relays';
-
-type SubCallback = (
- event: Event,
- relay: string,
-) => void;
-
-/** subscribe to global feed */
-export const subGlobalFeed = (onEvent: SubCallback) => {
- console.info('subscribe to global feed');
- unsubAll();
- const now = Math.floor(Date.now() * 0.001);
- const pubkeys = new Set();
- const notes = new Set();
- const prefix = Math.floor(config.filterDifficulty / 4); // 4 bits in each '0' character
- sub({ // get past events
- cb: (evt, relay) => {
- pubkeys.add(evt.pubkey);
- notes.add(evt.id);
- onEvent(evt, relay);
- },
- filter: {
- ...(prefix && {ids: ['0'.repeat(prefix)]}),
- kinds: [1],
- until: now,
- ...(!prefix && {since: Math.floor(now - (24 * 60 * 60))}),
- limit: 100,
- },
- unsub: true
- });
-
- setTimeout(() => {
- // get profile info
- sub({
- cb: onEvent,
- filter: {
- authors: Array.from(pubkeys),
- kinds: [0],
- limit: pubkeys.size,
- },
- unsub: true,
- });
- pubkeys.clear();
-
- notes.clear();
- }, 2000);
-
- // subscribe to future notes, reactions and profile updates
- sub({
- cb: (evt, relay) => {
- onEvent(evt, relay);
- if (
- evt.kind !== 1
- || pubkeys.has(evt.pubkey)
- ) {
- return;
- }
- },
- filter: {
- ...(prefix && {ids: ['0'.repeat(prefix)]}),
- kinds: [1],
- since: now,
- },
- });
-};
-
-/** subscribe to a note id (nip-19) */
-export const subNote = (
- eventId: string,
- onEvent: SubCallback,
-) => {
- unsubAll();
- sub({
- cb: onEvent,
- filter: {
- ids: [eventId],
- kinds: [1],
- limit: 1,
- },
- unsub: true,
- });
-
- const replies = new Set();
-
- const onReply = (evt: Event, relay: string) => {
- replies.add(evt.id)
- onEvent(evt, relay);
- unsubAll();
- sub({
- cb: onEvent,
- filter: {
- '#e': Array.from(replies),
- kinds: [1, 7],
- },
- unsub: true,
- });
- };
-
- replies.add(eventId);
- setTimeout(() => {
- sub({
- cb: onReply,
- filter: {
- '#e': [eventId],
- kinds: [1, 7],
- },
- unsub: true, // TODO: probably keep this subscription also after onReply/unsubAll
- });
- }, 200);
-};
-
-export const subEventID = (
- id: string,
- onEvent: SubCallback,
-) => {
- unsubAll();
- sub({
- cb: onEvent,
- filter: {
- ids: [id],
- limit: 1,
- },
- unsub: true,
- });
- sub({
- cb: onEvent,
- filter: {
- authors: [id],
- limit: 200,
- },
- unsub: true,
- });
-};
diff --git a/client/src/func/system.ts b/client/src/func/system.ts
deleted file mode 100644
index 1bb2b96..0000000
--- a/client/src/func/system.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import {Event, getEventHash, UnsignedEvent} from 'nostr-tools';
-import {elem, lockScroll, unlockScroll} from './utils/dom';
-
-const errorOverlay = document.querySelector('section#errorOverlay') as HTMLElement;
-
-type PromptErrorOptions = {
- onCancel?: () => void;
- onRetry?: () => void;
-};
-
-/**
- * Creates an error overlay, currently with hardcoded POW related message, this could be come a generic prompt
- * @param error message
- * @param options {onRetry, onCancel} callbacks
- */
-const promptError = (
- error: string,
- options: PromptErrorOptions,
-) => {
- const {onCancel, onRetry} = options;
- lockScroll();
- errorOverlay.replaceChildren(
- elem('h1', {className: 'error-title'}, error),
- elem('p', {}, 'time ran out finding a proof with the desired mining difficulty. either try again, lower the mining difficulty or increase the timeout in profile settings.'),
- elem('div', {className: 'buttons'}, [
- onCancel ? elem('button', {data: {action: 'close'}}, 'close') : '',
- onRetry ? elem('button', {data: {action: 'again'}}, 'try again') : '',
- ]),
- );
- const handleOverlayClick = (e: MouseEvent) => {
- if (e.target instanceof Element) {
- const button = e.target.closest('button');
- if (button) {
- switch(button.dataset.action) {
- case 'close':
- onCancel && onCancel();
- break;
- case 'again':
- onRetry && onRetry();
- break;
- }
- errorOverlay.removeEventListener('click', handleOverlayClick);
- errorOverlay.hidden = true;
- unlockScroll();
- }
- }
- };
- errorOverlay.addEventListener('click', handleOverlayClick);
- errorOverlay.hidden = false;
-}
-
-type PowEventOptions = {
- difficulty: number;
- statusElem: HTMLElement;
- timeout: number;
-};
-
-type WorkerResponse = {
- error: string;
- event: Event;
-};
-
-type HashedEvent = UnsignedEvent & {
- id: string;
-};
-
-/**
- * run proof of work in a worker until at least the specified difficulty.
- * if succcessful, the returned event contains the 'nonce' tag
- * and the updated created_at timestamp.
- *
- * powEvent returns a rejected promise if the funtion runs for longer than timeout.
- * a zero timeout makes mineEvent run without a time limit.
- * a zero mining target just resolves the promise without trying to find a 'nonce'.
- */
-export const powEvent = (
- evt: UnsignedEvent,
- options: PowEventOptions
-): Promise => {
- const {difficulty, statusElem, timeout} = options;
- if (difficulty === 0) {
- return Promise.resolve({
- ...evt,
- id: getEventHash(evt),
- });
- }
- const cancelBtn = elem('button', {className: 'btn-inline'}, [elem('small', {}, 'cancel')]);
- statusElem.replaceChildren('working…', cancelBtn);
- statusElem.hidden = false;
- return new Promise((resolve, reject) => {
- const worker = new Worker('/worker.js');
-
- const onCancel = () => {
- worker.terminate();
- reject(`mining kind ${evt.kind} event canceled`);
- };
- cancelBtn.addEventListener('click', onCancel);
-
- worker.onmessage = (msg: MessageEvent) => {
- worker.terminate();
- cancelBtn.removeEventListener('click', onCancel);
- if (msg.data.error) {
- promptError(msg.data.error, {
- onCancel: () => reject(`mining kind ${evt.kind} event canceled`),
- onRetry: async () => {
- const result = await powEvent(evt, {difficulty, statusElem, timeout}).catch(console.warn);
- resolve(result);
- }
- })
- } else {
- resolve(msg.data.event);
- }
- };
-
- worker.onerror = (err) => {
- worker.terminate();
- // promptError(msg.data.error, {});
- cancelBtn.removeEventListener('click', onCancel);
- reject(err);
- };
-
- worker.postMessage({event: evt, difficulty, timeout});
- });
-};
diff --git a/client/src/func/worker.js b/client/src/func/worker.js
deleted file mode 100644
index c9007b2..0000000
--- a/client/src/func/worker.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import {getEventHash} from 'nostr-tools';
-import {zeroLeadingBitsCount} from './utils/crypto';
-
-const mine = (event, difficulty, timeout = 5) => {
- const max = 256; // arbitrary
- if (!Number.isInteger(difficulty) || difficulty < 0 || difficulty > max) {
- throw new Error(`difficulty must be an integer between 0 and ${max}`);
- }
- // continue with mining
- let n = BigInt(0);
- event.tags.unshift(['nonce', n.toString(), `${difficulty}`]);
-
- const until = Math.floor(Date.now() * 0.001) + timeout;
- console.time('pow');
- while (true) {
- const now = Math.floor(Date.now() * 0.001);
- if (timeout !== 0 && (now > until)) {
- console.timeEnd('pow');
- throw 'timeout';
- }
- if (now !== event.created_at) {
- event.created_at = now;
- // n = BigInt(0); // could reset nonce as we have a new timestamp
- }
- event.tags[0][1] = (++n).toString();
- const id = getEventHash(event);
- if (zeroLeadingBitsCount(id) === difficulty) {
- console.timeEnd('pow');
- return {id, ...event};
- }
- }
-};
-
-addEventListener('message', (msg) => {
- const {difficulty, event, timeout} = msg.data;
- try {
- const minedEvent = mine(event, difficulty, timeout);
- postMessage({event: minedEvent});
- } catch (err) {
- postMessage({error: err});
- }
-});
\ No newline at end of file
diff --git a/client/src/utils/crypto.ts b/client/src/utils/crypto.ts
deleted file mode 100644
index 8ea6b85..0000000
--- a/client/src/utils/crypto.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * evaluate the difficulty of hex32 according to nip-13.
- * @param hex32 a string of 64 chars - 32 bytes in hex representation
- */
-export const zeroLeadingBitsCount = (hex32: string) => {
- let count = 0;
- for (let i = 0; i < 64; i += 2) {
- const hexbyte = hex32.slice(i, i + 2); // grab next byte
- if (hexbyte === '00') {
- count += 8;
- continue;
- }
- // reached non-zero byte; count number of 0 bits in hexbyte
- const bits = parseInt(hexbyte, 16).toString(2).padStart(8, '0');
- for (let b = 0; b < 8; b++) {
- if (bits[b] === '1' ) {
- break; // reached non-zero bit; stop
- }
- count += 1;
- }
- break;
- }
- return count;
- };
-
\ No newline at end of file