adding stuff
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,6 +3,8 @@ node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/sanitize-html": "^2.11.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
@@ -33,8 +35,11 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@nostr-dev-kit/ndk": "^2.8.2",
|
||||
"@nostr-dev-kit/ndk-svelte": "^2.2.15",
|
||||
"@nostr-dev-kit/ndk-svelte-components": "^2.2.16"
|
||||
"@nostr-dev-kit/ndk-svelte-components": "^2.2.16",
|
||||
"markdown-it": "^14.1.0",
|
||||
"sanitize-html": "^2.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
26
src/app.html
26
src/app.html
@@ -1,12 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<html data-theme="dark" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<main class="container">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
15
src/lib/ndk-kinds/index.ts
Normal file
15
src/lib/ndk-kinds/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export enum NDKKind {
|
||||
Highlight = 9802,
|
||||
UserList = 30000,
|
||||
GenericList = 30001,
|
||||
HighlightList = 39802,
|
||||
RelayList = 30022,
|
||||
LongForm = 30023,
|
||||
}
|
||||
|
||||
export const NDKListKinds = [
|
||||
NDKKind.UserList,
|
||||
NDKKind.GenericList,
|
||||
NDKKind.RelayList,
|
||||
NDKKind.HighlightList,
|
||||
];
|
||||
184
src/lib/ndk-kinds/lists/index.ts
Normal file
184
src/lib/ndk-kinds/lists/index.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import NDK, { NDKEvent, NDKUser, type NDKTag, type NostrEvent, NDKRelay, type NDKSigner, type NDKFilter, mergeFilters } from '@nostr-dev-kit/ndk';
|
||||
import { NDKKind } from '../index';
|
||||
import { filterForId } from '$lib/utils/index';
|
||||
|
||||
/**
|
||||
* Represents any NIP-33 list kind.
|
||||
*/
|
||||
class NDKList extends NDKEvent {
|
||||
public _encryptedTags: NDKTag[] | undefined;
|
||||
|
||||
/**
|
||||
* Stores the number of bytes the content was before decryption
|
||||
* to expire the cache when the content changes.
|
||||
*/
|
||||
private encryptedTagsLength: number | undefined;
|
||||
|
||||
constructor(ndk?: NDK, rawEvent?: NostrEvent) {
|
||||
super(ndk, rawEvent);
|
||||
if (!this.kind) this.kind = NDKKind.GenericList;
|
||||
}
|
||||
|
||||
static from(ndkEvent: NDKEvent): NDKList {
|
||||
return new NDKList(ndkEvent.ndk, ndkEvent.rawEvent());
|
||||
}
|
||||
|
||||
get name(): string | undefined {
|
||||
return this.tagValue('name') ?? this.tagValue('d');
|
||||
}
|
||||
|
||||
set name(name: string | undefined) {
|
||||
this.removeTag('name');
|
||||
|
||||
if (name) {
|
||||
this.tags.push(['name', name]);
|
||||
} else {
|
||||
throw new Error('Name cannot be empty');
|
||||
}
|
||||
}
|
||||
|
||||
get description(): string | undefined {
|
||||
return this.tagValue('description');
|
||||
}
|
||||
|
||||
set description(name: string | undefined) {
|
||||
if (name) {
|
||||
this.tags.push(['description', name]);
|
||||
} else {
|
||||
this.removeTag('description');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decrypted content of the list.
|
||||
*/
|
||||
async encryptedTags(useCache = true): Promise<NDKTag[]> {
|
||||
if (this._encryptedTags && this.encryptedTagsLength === this.content.length && useCache) return this._encryptedTags;
|
||||
|
||||
if (!this.ndk) throw new Error('NDK instance not set');
|
||||
if (!this.ndk.signer) throw new Error('NDK signer not set');
|
||||
|
||||
const user = await this.ndk.signer.user();
|
||||
|
||||
try {
|
||||
if (this.content.length > 0) {
|
||||
try {
|
||||
console.log(`decrypting ${this.content}`);
|
||||
const decryptedContent = await this.ndk.signer.decrypt(user, this.content);
|
||||
const a = JSON.parse(decryptedContent);
|
||||
if (a && a[0]) {
|
||||
this.encryptedTagsLength = this.content.length;
|
||||
return this._encryptedTags = a;
|
||||
}
|
||||
this.encryptedTagsLength = this.content.length;
|
||||
return this._encryptedTags = [];
|
||||
} catch (e) {
|
||||
console.log(`error decrypting ${this.content}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// console.trace(e);
|
||||
// throw e;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public validateTag(tagValue: string): boolean | string {
|
||||
return true;
|
||||
}
|
||||
|
||||
get items(): NDKTag[] {
|
||||
return this.tags.filter((t) => {
|
||||
return !['d', 'name', 'description'].includes(t[0]);
|
||||
});
|
||||
}
|
||||
|
||||
async fetchItems(): Promise<Set<NDKEvent>> {
|
||||
let filters: NDKFilter[] = []
|
||||
|
||||
if (!this.ndk) throw new Error('NDK instance not set');
|
||||
|
||||
filters = this.items.map(i => filterForId(i[1]));
|
||||
|
||||
console.log(`filters: ${JSON.stringify(filters)}`);
|
||||
console.log(mergeFilters(filters));
|
||||
|
||||
return this.ndk.fetchEvents(
|
||||
mergeFilters(filters)
|
||||
);
|
||||
}
|
||||
|
||||
async addItem(relay: NDKRelay, mark?: string, encrypted?: boolean): Promise<void>;
|
||||
async addItem(event: NDKEvent, mark?: string, encrypted?: boolean): Promise<void>;
|
||||
async addItem(user: NDKUser, mark?: string, encrypted?: boolean): Promise<void>;
|
||||
async addItem(tag: NDKTag, mark?: string, encrypted?: boolean): Promise<void>;
|
||||
async addItem(obj: NDKUser | NDKEvent | NDKRelay | NDKTag, mark: string | undefined = undefined, encrypted: boolean = false): Promise<void> {
|
||||
if (!this.ndk) throw new Error('NDK instance not set');
|
||||
if (!this.ndk.signer) throw new Error('NDK signer not set');
|
||||
|
||||
let tag;
|
||||
|
||||
if (obj instanceof NDKEvent) {
|
||||
tag = obj.tagReference();
|
||||
} else if (obj instanceof NDKUser) {
|
||||
tag = obj.tagReference();
|
||||
} else if (obj instanceof NDKRelay) {
|
||||
tag = ['r', (obj as NDKRelay).url];
|
||||
} else if (Array.isArray(obj)) {
|
||||
// NDKTag
|
||||
tag = obj;
|
||||
} else {
|
||||
throw new Error('Invalid object type');
|
||||
}
|
||||
|
||||
if (mark) tag.push(mark);
|
||||
|
||||
if (encrypted) {
|
||||
const user = await this.ndk.signer.user();
|
||||
const currentList = await this.encryptedTags();
|
||||
|
||||
console.log(`current list: ${JSON.stringify(currentList)}`)
|
||||
|
||||
currentList.push(tag);
|
||||
this._encryptedTags = currentList;
|
||||
this.encryptedTagsLength = this.content.length;
|
||||
this.content = JSON.stringify(currentList);
|
||||
await this.encrypt(user);
|
||||
} else {
|
||||
this.tags.push(tag);
|
||||
}
|
||||
|
||||
this.created_at = Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the list.
|
||||
*
|
||||
* @param index The index of the item to remove.
|
||||
* @param encrypted Whether to remove from the encrypted list or not.
|
||||
*/
|
||||
async removeItem(index: number, encrypted: boolean): Promise<NDKList> {
|
||||
if (!this.ndk) throw new Error('NDK instance not set');
|
||||
if (!this.ndk.signer) throw new Error('NDK signer not set');
|
||||
|
||||
if (encrypted) {
|
||||
const user = await this.ndk.signer.user();
|
||||
const currentList = await this.encryptedTags();
|
||||
|
||||
currentList.splice(index, 1);
|
||||
this._encryptedTags = currentList;
|
||||
this.encryptedTagsLength = this.content.length;
|
||||
this.content = JSON.stringify(currentList);
|
||||
await this.encrypt(user);
|
||||
} else {
|
||||
this.tags.splice(index, 1);
|
||||
}
|
||||
|
||||
this.created_at = Math.floor(Date.now() / 1000);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default NDKList;
|
||||
31
src/lib/ndk-kinds/lists/relay-list.ts
Normal file
31
src/lib/ndk-kinds/lists/relay-list.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||
import NDKList from './index.js';
|
||||
import type NDK from '@nostr-dev-kit/ndk';
|
||||
|
||||
class NDKRelayList extends NDKList {
|
||||
constructor(ndk?: NDK, rawEvent?: NostrEvent) {
|
||||
super(ndk, rawEvent);
|
||||
}
|
||||
|
||||
static from(ndkEvent: NDKEvent) {
|
||||
return new NDKRelayList(ndkEvent.ndk, ndkEvent.rawEvent());
|
||||
}
|
||||
|
||||
public validateTag(tagValue: string): boolean | string {
|
||||
if (tagValue.startsWith('wss://')) return true;
|
||||
if (tagValue.startsWith('nrelay')) return true;
|
||||
|
||||
return 'Invalid relay URL';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of relays
|
||||
*/
|
||||
get relays(): string[] {
|
||||
console.log(this.getMatchingTags('r'));
|
||||
|
||||
return this.getMatchingTags('r').map((tag) => tag[1]);
|
||||
}
|
||||
}
|
||||
|
||||
export default NDKRelayList;
|
||||
142
src/lib/signers/ephemeral.ts
Normal file
142
src/lib/signers/ephemeral.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import NDK, { NDKEvent, NDKPrivateKeySigner, type NDKFilter, type NDKSigner, type NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||
import type { NDKTag, NostrEvent} from "@nostr-dev-kit/ndk";
|
||||
import { sha256 } from "@noble/hashes/sha256";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
|
||||
const CURRENT_PRIVATE_NOTE_VERSION = '2';
|
||||
|
||||
interface IFindEphemeralSignerLookups {
|
||||
name?: string,
|
||||
associatedEventNip19?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a named ephemeral signer from a self-DM.
|
||||
*/
|
||||
export async function findEphemeralSigner(
|
||||
ndk: NDK,
|
||||
mainSigner: NDKSigner,
|
||||
opts: IFindEphemeralSignerLookups
|
||||
): Promise<NDKPrivateKeySigner | undefined> {
|
||||
const mainUser = await mainSigner.user();
|
||||
const filter: NDKFilter = { kinds: [2600 as number]};
|
||||
|
||||
if (opts.name) {
|
||||
const hashedName = await getHashedKeyName(opts.name);
|
||||
filter['#e'] = [hashedName];
|
||||
} else if (opts.associatedEventNip19) {
|
||||
const hashedEventReference = await getHashedKeyName(opts.associatedEventNip19);
|
||||
filter['#e'] = [hashedEventReference];
|
||||
}
|
||||
|
||||
const event = await ndk.fetchEvent(filter);
|
||||
|
||||
if (event) {
|
||||
const decryptEventFunction = async (event: NDKEvent) => {
|
||||
await event.decrypt(await mainSigner.user());
|
||||
const content = JSON.parse(event.content);
|
||||
return new NDKPrivateKeySigner(content.key);
|
||||
};
|
||||
|
||||
const promise = new Promise<NDKPrivateKeySigner>((resolve, reject) => {
|
||||
let decryptionAttempts = 0;
|
||||
try {
|
||||
decryptionAttempts++;
|
||||
resolve(decryptEventFunction(event));
|
||||
} catch(e) {
|
||||
if (decryptionAttempts> 5) {
|
||||
console.error(`Failed to decrypt ephemeral signer event after ${decryptionAttempts} attempts`);
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => { decryptEventFunction(event);}, 1000 * Math.random());
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
type EphemeralKeyEventContent = {
|
||||
key: string;
|
||||
event?: string;
|
||||
version: string;
|
||||
metadata?: object;
|
||||
}
|
||||
|
||||
interface ISaveOpts {
|
||||
associatedEvent?: NDKEvent;
|
||||
name?: string;
|
||||
metadata?: object;
|
||||
keyProfile?: NDKUserProfile;
|
||||
mainSigner?: NDKSigner;
|
||||
}
|
||||
|
||||
function generateContent(targetSigner: NDKPrivateKeySigner, opts: ISaveOpts = {}) {
|
||||
const content: EphemeralKeyEventContent = {
|
||||
key: targetSigner.privateKey!,
|
||||
version: CURRENT_PRIVATE_NOTE_VERSION,
|
||||
...opts.metadata,
|
||||
};
|
||||
if (opts.associatedEvent) content.event = opts.associatedEvent.encode();
|
||||
|
||||
return JSON.stringify(content);
|
||||
}
|
||||
|
||||
async function getHashedKeyName(name:string) {
|
||||
let eventHash = sha256(name);
|
||||
return bytesToHex(eventHash);
|
||||
}
|
||||
|
||||
async function generateTags(mainSigner:NDKSigner, opts: ISaveOpts = {}) {
|
||||
const mainUser = await mainSigner.user();
|
||||
const tags = [
|
||||
['p', mainUser.pubkey],
|
||||
['client', 'cofabricate'],
|
||||
]
|
||||
|
||||
if (opts.associatedEvent) {
|
||||
const hashedEventReference = await getHashedKeyName(opts.associatedEvent.encode());
|
||||
tags.push(['e', hashedEventReference]);
|
||||
}
|
||||
|
||||
if (opts.name) {
|
||||
const hashedName = await getHashedKeyName(opts.name);
|
||||
tags.push(['e', hashedName]);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
export async function saveEphemeralSigner(ndk: NDK, targetSigner: NDKPrivateKeySigner, opts: ISaveOpts = {}) {
|
||||
const mainSigner = opts.mainSigner || ndk.signer;
|
||||
|
||||
if (!mainSigner) throw new Error('No main signer provided');
|
||||
|
||||
const mainUser = await mainSigner.user();
|
||||
const event = new NDKEvent(ndk, {
|
||||
kind: 2600,
|
||||
content: generateContent(targetSigner, opts),
|
||||
tags: await generateTags(mainSigner, opts),
|
||||
} as NostrEvent);
|
||||
|
||||
event.pubkey = mainUser.pubkey;
|
||||
await event.encrypt(mainUser, mainSigner);
|
||||
await event.publish();
|
||||
|
||||
if (opts.keyProfile) {
|
||||
const user = await targetSigner.user();
|
||||
const event = new NDKEvent(ndk, {
|
||||
kind: 0,
|
||||
content: JSON.stringify(opts.keyProfile),
|
||||
tags: [] as NDKTag[],
|
||||
} as NostrEvent)
|
||||
event.pubkey = user.pubkey;
|
||||
await event.sign(targetSigner);
|
||||
await event.publish();
|
||||
}
|
||||
}
|
||||
|
||||
export function generateEphemeralSigner(): NDKPrivateKeySigner {
|
||||
const signer = NDKPrivateKeySigner.generate();
|
||||
return signer;
|
||||
}
|
||||
24
src/lib/store.ts
Normal file
24
src/lib/store.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { NDKUser } from "@nostr-dev-kit/ndk";
|
||||
|
||||
export const currentUser = writable<NDKUser | null>(null);
|
||||
export const currentUserFollowPubkeys = writable<string[] | undefined>(undefined);
|
||||
export const backgroundBanner = writable<string | null>(null);
|
||||
|
||||
export type ScopeSelection = {
|
||||
label: string;
|
||||
id: string;
|
||||
pubkeys: string[] | undefined;
|
||||
};
|
||||
export const currentScope = writable<ScopeSelection>({
|
||||
label: 'global',
|
||||
id: 'global',
|
||||
pubkeys: undefined,
|
||||
});
|
||||
|
||||
let zapEvent: any;
|
||||
|
||||
export const zap = writable(zapEvent);
|
||||
|
||||
export const pageTitle = writable<string | null>(null);
|
||||
export const pageSubtitle = writable<string | null>(null);
|
||||
35
src/lib/stores/nostr.ts
Normal file
35
src/lib/stores/nostr.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { writable } from "svelte/store";
|
||||
import NDK from "@nostr-dev-kit/ndk";
|
||||
import NDKSvelte from "@nostr-dev-kit/ndk-svelte";
|
||||
import { RelayList } from "@nostr-dev-kit/ndk-svelte-components";
|
||||
|
||||
let relays;
|
||||
|
||||
try {
|
||||
relays = localStorage.getItem('relays');
|
||||
} catch (e) {}
|
||||
|
||||
let relayList: string[] = [];
|
||||
|
||||
if (relays) {
|
||||
relayList = JSON.parse(relays);
|
||||
}
|
||||
|
||||
export const defaultRelays = [
|
||||
'wss://purplepag.es',
|
||||
'wss://relay.damus.io'
|
||||
]
|
||||
|
||||
if (!relayList || !Array.isArray(relayList) || relayList.length === 0) {
|
||||
relayList = defaultRelays;
|
||||
}
|
||||
|
||||
const _ndk: NDKSvelte = new NDKSvelte({
|
||||
devWriteRelayUrls: ['wss://relay.strfront.com'],
|
||||
explicitRelayUrls: relayList,
|
||||
enableOutboxModel: true,
|
||||
}) as NDKSvelte;
|
||||
|
||||
const ndk = writable(_ndk);
|
||||
|
||||
export default ndk;
|
||||
82
src/lib/stores/signer.ts
Normal file
82
src/lib/stores/signer.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { findEphemeralSigner } from "$lib/signers/ephemeral";
|
||||
import { NDKPrivateKeySigner, type NDKSigner, type NDKUser } from "@nostr-dev-kit/ndk";
|
||||
import { writable, get as getStore, derived } from "svelte/store";
|
||||
import ndkStore from "./nostr";
|
||||
import { currentUser as currentUserStore } from "../store";
|
||||
import type NDKList from "$lib/ndk-kinds/lists";
|
||||
|
||||
export type SignerStoreItem = {
|
||||
signer: NDKPrivateKeySigner;
|
||||
user: NDKUser;
|
||||
saved: boolean;
|
||||
name?: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
type SignerItems = Map<string, SignerStoreItem>;
|
||||
|
||||
export const signers = writable<SignerItems>(new Map());
|
||||
export const npubSigners = derived(signers, ($signers) => {
|
||||
const npubs = new Map<string, NDKSigner>();
|
||||
|
||||
for (const entry of $signers) {
|
||||
const { user, signer } = entry[1];
|
||||
|
||||
npubs.set(user.npub, signer);
|
||||
}
|
||||
|
||||
return npubs;
|
||||
});
|
||||
|
||||
async function getDelegatedSignerName(list: NDKList) {
|
||||
let name = '';
|
||||
const currentUser: NDKUser = getStore(currentUserStore);
|
||||
|
||||
if (!currentUser?.profile) {
|
||||
currentUser.ndk = getStore(ndkStore);
|
||||
await currentUser?.fetchProfile();
|
||||
}
|
||||
|
||||
if (currentUser?.profile?.name) {
|
||||
name = currentUser.profile.displayName + `'s `;
|
||||
}
|
||||
|
||||
return name + list.name;
|
||||
}
|
||||
|
||||
export async function getSigner(list: NDKList): Promise<SignerStoreItem> {
|
||||
const store = getStore(signers);
|
||||
const id = list.encode();
|
||||
let item = store.get(id);
|
||||
|
||||
if (item) return item;
|
||||
|
||||
const ndk = getStore(ndkStore);
|
||||
let signer = await findEphemeralSigner(ndk, ndk.signer!, {
|
||||
associatedEventNip19: list.encode(),
|
||||
});
|
||||
|
||||
if (signer) {
|
||||
console.log(`found a signer for list ${list.name}`);
|
||||
item = {
|
||||
signer: signer!,
|
||||
user: await signer.user(),
|
||||
saved: true,
|
||||
id,
|
||||
};
|
||||
} else {
|
||||
signer = NDKPrivateKeySigner.generate();
|
||||
item = {
|
||||
signer,
|
||||
user: await signer.user(),
|
||||
saved: false,
|
||||
name: await getDelegatedSignerName(list),
|
||||
id,
|
||||
};
|
||||
}
|
||||
item.user.ndk = ndk;
|
||||
|
||||
store.set(id, item);
|
||||
|
||||
return item;
|
||||
}
|
||||
190
src/lib/utils/index.ts
Normal file
190
src/lib/utils/index.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { filterFromId, type NDKFilter, type NDKTag } from '@nostr-dev-kit/ndk';
|
||||
|
||||
export function tagToNip19(tag: NDKTag): string | undefined {
|
||||
switch (tag[0]) {
|
||||
case 'a':
|
||||
const [ kind, pubkey, identifier ] = tag[1].split(':');
|
||||
return nip19.naddrEncode({
|
||||
kind: parseInt(kind),
|
||||
pubkey,
|
||||
identifier
|
||||
})
|
||||
case 'e':
|
||||
return nip19.noteEncode(tag[1]);
|
||||
case 'r':
|
||||
return tag[1];
|
||||
}
|
||||
}
|
||||
|
||||
export function nicelyFormattedMilliSatNumber(amount: number) {
|
||||
return nicelyFormattedSatNumber(
|
||||
Math.floor(amount / 1000)
|
||||
);
|
||||
}
|
||||
|
||||
export function nicelyFormattedSatNumber(amount: number) {
|
||||
// if the number is less than 1000, just return it
|
||||
if (amount < 1000) return amount;
|
||||
|
||||
// if the number is less than 1 million, return it with a k, if the comma is not needed remove it
|
||||
if (amount < 1000000) return `${(amount / 1000).toFixed(0)}k`;
|
||||
|
||||
// if the number is less than 1 billion, return it with an m
|
||||
if (amount < 1000000000) return `${(amount / 1000000).toFixed(1)}m`;
|
||||
|
||||
return `${(amount / 100_000_000).toFixed(2)} btc`;
|
||||
}
|
||||
|
||||
export function filterForId(id: string): NDKFilter {
|
||||
if (!!id.match(/:/)) {
|
||||
const [kind, pubkey, identifier] = id.split(':');
|
||||
return { kinds: [parseInt(kind)], authors: [pubkey], '#d': [identifier] };
|
||||
} else {
|
||||
return { ids: [id] };
|
||||
}
|
||||
}
|
||||
|
||||
export function filterFromNaddr(naddr: string): NDKFilter {
|
||||
return filterFromId(naddr);
|
||||
}
|
||||
|
||||
export function idFromNaddr(naddr: string) {
|
||||
const ndecode = nip19.decode(naddr).data as any;
|
||||
return `${ndecode.kind}:${ndecode.pubkey}:${ndecode.identifier}`;
|
||||
}
|
||||
|
||||
export function naddrFromTagValue(value: string) {
|
||||
const [kind, pubkey, identifier] = value.split(':');
|
||||
|
||||
return nip19.naddrEncode({
|
||||
kind: parseInt(kind),
|
||||
pubkey,
|
||||
identifier
|
||||
});
|
||||
}
|
||||
|
||||
export function prettifyContent(content: string) {
|
||||
const bitcoinImage =
|
||||
'<img src="https://abs.twimg.com/hashflags/Bitcoin_evergreen/Bitcoin_evergreen.png" style="width: 1.2em; vertical-align: -20%; margin-right: 0.075em; height: 1.2em; margin-left: 2px; display: inline-block;">';
|
||||
|
||||
content = content.replace(/#bitcoin/i, `#bitcoin${bitcoinImage}`);
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
autolink: true,
|
||||
image: true,
|
||||
});
|
||||
md.linkify?.set();
|
||||
content = md.render(content);
|
||||
|
||||
return sanitizeHtml(content);
|
||||
}
|
||||
|
||||
function flattenText(node: string) {
|
||||
const texts = [];
|
||||
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
|
||||
|
||||
while (walker.nextNode()) {
|
||||
texts.push(walker.currentNode);
|
||||
}
|
||||
|
||||
return texts;
|
||||
}
|
||||
|
||||
export function highlightText(targetText: string, highlightId: string) {
|
||||
const regex = new RegExp(escapeRegExp(targetText), 'g');
|
||||
const textNodes = flattenText(document.body);
|
||||
const marks: HTMLElement[] = [];
|
||||
|
||||
textNodes.forEach((textNode) => {
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((match = regex.exec(textNode.data)) !== null) {
|
||||
const mark = document.createElement('mark');
|
||||
mark.setAttribute('data-highlight-id', highlightId);
|
||||
const range = document.createRange();
|
||||
const startOffset = match.index;
|
||||
const endOffset = startOffset + targetText.length;
|
||||
|
||||
if (lastIndex < startOffset) {
|
||||
const precedingTextNode = document.createTextNode(textNode.data.slice(lastIndex, startOffset));
|
||||
textNode.parentNode.insertBefore(precedingTextNode, textNode);
|
||||
}
|
||||
|
||||
range.setStart(textNode, startOffset);
|
||||
range.setEnd(textNode, endOffset);
|
||||
range.surroundContents(mark);
|
||||
marks.push(mark);
|
||||
|
||||
lastIndex = endOffset;
|
||||
}
|
||||
|
||||
if (lastIndex < textNode.length) {
|
||||
const remainingTextNode = document.createTextNode(textNode.data.slice(lastIndex));
|
||||
textNode.parentNode.insertBefore(remainingTextNode, textNode);
|
||||
}
|
||||
|
||||
textNode.remove();
|
||||
});
|
||||
|
||||
return marks;
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
}
|
||||
|
||||
// Usage example: highlightText("your target text here");
|
||||
|
||||
// export function modifyDocument(content: string) {
|
||||
// const regex = new RegExp(content, 'gi');
|
||||
|
||||
// // get all the text nodes in the current page
|
||||
// const textNodes = document.evaluate("//text()", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
|
||||
|
||||
// // loop through all the text nodes and replace the search string with the marked up version
|
||||
// for (let i = 0; i < textNodes.snapshotLength; i++) {
|
||||
// const node = textNodes.snapshotItem(i) as Text;
|
||||
// const parent = node.parentNode;
|
||||
|
||||
// // check if the text node contains the search string
|
||||
// if (!node || !node.textContent || !node.textContent.match(regex)) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// const fragment = document.createDocumentFragment();
|
||||
// let match;
|
||||
|
||||
// if (!node) continue;
|
||||
|
||||
// // loop through all the matches in the text node
|
||||
// while ((match = regex.exec(node.textContent)) !== null) {
|
||||
// console.log({match, node});
|
||||
|
||||
// const before = node.splitText(match.index);
|
||||
// const after = node.splitText(match[0].length);
|
||||
|
||||
// console.log({before, after});
|
||||
|
||||
// const mark = document.createElement("mark");
|
||||
|
||||
// // append the matched text to the mark element
|
||||
// mark.appendChild(document.createTextNode(match[0]));
|
||||
|
||||
// // append the mark element to the document fragment
|
||||
// fragment.appendChild(before);
|
||||
// fragment.appendChild(mark);
|
||||
|
||||
// node = after;
|
||||
// }
|
||||
|
||||
// // replace the original text node with the marked up version
|
||||
// parent.replaceChild(fragment, node);
|
||||
// }
|
||||
// }
|
||||
11
src/routes/+layout.svelte
Normal file
11
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<nav>
|
||||
<ul>
|
||||
<li><h1>Welcome to CoFabricate</h1></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/login">Sign In</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<slot></slot>
|
||||
@@ -1 +1,5 @@
|
||||
<h1>Welcome to CoFabricate</h1>
|
||||
<svelte:head>
|
||||
<title>Decentralized Manufacturing</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Home</h1>
|
||||
5
src/routes/about/+page.svelte
Normal file
5
src/routes/about/+page.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<svelte:head>
|
||||
<title>About</title>
|
||||
</svelte:head>
|
||||
|
||||
<p>About page</p>
|
||||
5
src/routes/login/+page.svelte
Normal file
5
src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<svelte:head>
|
||||
<title>Log In</title>
|
||||
</svelte:head>
|
||||
|
||||
<p>Log In</p>n
|
||||
Reference in New Issue
Block a user