clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

290 lines
7.7 KiB

import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
import { getMatchingTags } from "./utils/nostrUtils.ts";
import type { AddressPointer, EventPointer } from "nostr-tools/nip19";
import type { NostrEvent } from "./utils/websocket_utils.ts";
export class DecodeError extends Error {
constructor(message: string) {
super(message);
this.name = "DecodeError";
}
}
export class InvalidKindError extends DecodeError {
constructor(message: string) {
super(message);
this.name = "InvalidKindError";
}
}
export function neventEncode(event: NDKEvent, relays: string[]) {
return nip19.neventEncode({
id: event.id,
kind: event.kind,
relays,
author: event.pubkey,
});
}
export function naddrEncode(event: NDKEvent, relays: string[]) {
const dTag = getMatchingTags(event, "d")[0]?.[1];
if (!dTag) {
throw new Error("Event does not have a d tag");
}
return nip19.naddrEncode({
identifier: dTag,
pubkey: event.pubkey,
kind: event.kind || 0,
relays,
});
}
/**
* Creates a tag address from a raw Nostr event (for compatibility with NDK events)
* @param event The raw Nostr event
* @param relays Optional relay list for the address
* @returns A tag address string
*/
export function createTagAddress(event: NostrEvent, relays: string[] = []): string {
const dTag = event.tags.find((tag: string[]) => tag[0] === "d")?.[1];
if (!dTag) {
throw new Error("Event does not have a d tag");
}
return nip19.naddrEncode({
identifier: dTag,
pubkey: event.pubkey,
kind: event.kind,
relays,
});
}
export function nprofileEncode(pubkey: string, relays: string[]) {
return nip19.nprofileEncode({ pubkey, relays });
}
/**
* Decodes a nostr identifier (naddr, nevent) and returns the decoded data.
* @param identifier The nostr identifier to decode.
* @param expectedType The expected type of the decoded data ('naddr' or 'nevent').
* @returns The decoded data.
*/
function decodeNostrIdentifier<T extends AddressPointer | EventPointer>(
identifier: string,
expectedType: "naddr" | "nevent",
): T {
try {
if (!identifier.startsWith(expectedType)) {
throw new InvalidKindError(`Invalid ${expectedType} format`);
}
const decoded = nip19.decode(identifier);
if (decoded.type !== expectedType) {
throw new InvalidKindError(`Decoded result is not an ${expectedType}`);
}
return decoded.data as T;
} catch (error) {
throw new DecodeError(`Failed to decode ${expectedType}: ${error}`);
}
}
/**
* Decodes an naddr identifier and returns the decoded data
*/
export function naddrDecode(naddr: string): AddressPointer {
return decodeNostrIdentifier<AddressPointer>(naddr, "naddr");
}
/**
* Decodes an nevent identifier and returns the decoded data
*/
export function neventDecode(nevent: string): EventPointer {
return decodeNostrIdentifier<EventPointer>(nevent, "nevent");
}
export function formatDate(unixtimestamp: number) {
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const date = new Date(unixtimestamp * 1000);
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear();
const formattedDate = `${day} ${month} ${year}`;
return formattedDate;
}
let serial = 0;
export function next(): number {
serial++;
return serial;
}
export function scrollTabIntoView(el: string | HTMLElement, wait: boolean) {
function scrollTab() {
const element =
typeof el === "string"
? document.querySelector(`[id^="wikitab-v0-${el}"]`)
: el;
if (!element) return;
element.scrollIntoView({
behavior: "smooth",
inline: "start",
});
}
if (wait) {
setTimeout(() => {
scrollTab();
}, 1);
} else {
scrollTab();
}
}
export function isElementInViewport(el: string | HTMLElement) {
const element =
typeof el === "string"
? document.querySelector(`[id^="wikitab-v0-${el}"]`)
: el;
if (!element) return;
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(globalThis.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (globalThis.innerWidth || document.documentElement.clientWidth)
);
}
/**
* Removes `kind: 30040` index events that don't comply with the NKBIP-01 specification.
* @param events A set of events.
* @returns The filtered set of events.
*/
export function filterValidIndexEvents(events: Set<NDKEvent>): Set<NDKEvent> {
// The filter object supports only limited parameters, so we need to filter out events that
// don't respect NKBIP-01.
events.forEach((event) => {
// Index events have no content, and they must have `title`, `d`, and `e` tags.
if (
(event.content != null && event.content.length > 0) ||
getMatchingTags(event, "title").length === 0 ||
getMatchingTags(event, "d").length === 0 ||
(getMatchingTags(event, "a").length === 0 &&
getMatchingTags(event, "e").length === 0)
) {
events.delete(event);
}
});
console.debug(`Filtered index events: ${events.size} events remaining.`);
return events;
}
/**
* Async version of Array.findIndex() that runs sequentially.
* Returns the index of the first element that satisfies the provided testing function.
* @param array The array to search
* @param predicate The async testing function
* @returns A promise that resolves to the index of the first matching element, or -1 if none found
*/
export async function findIndexAsync<T>(
array: T[],
predicate: (element: T, index: number, array: T[]) => Promise<boolean>,
): Promise<number> {
for (let i = 0; i < array.length; i++) {
if (await predicate(array[i], i, array)) {
return i;
}
}
return -1;
}
// Extend Array prototype with findIndexAsync
declare global {
interface Array<T> {
findIndexAsync(
predicate: (element: T, index: number, array: T[]) => Promise<boolean>,
): Promise<number>;
}
}
Array.prototype.findIndexAsync = function <T>(
this: T[],
predicate: (element: T, index: number, array: T[]) => Promise<boolean>,
): Promise<number> {
return findIndexAsync(this, predicate);
};
/**
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed
* since the last time the debounced function was invoked.
* @param func The function to debounce
* @param wait The number of milliseconds to delay
* @returns A debounced version of the function
*/
// deno-lint-ignore no-explicit-any
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | undefined;
return function executedFunction(...args: Parameters<T>) {
const later = () => {
timeout = undefined;
func(...args);
};
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
};
}
/**
* Creates a debounced async function that delays invoking func until after wait milliseconds have elapsed
* since the last time the debounced function was invoked.
* @param func The async function to debounce
* @param wait The number of milliseconds to delay
* @returns A debounced version of the async function
*/
export function debounceAsync(
func: (query: string) => Promise<void>,
wait: number,
): (query: string) => void {
let timeout: ReturnType<typeof setTimeout> | undefined;
return function executedFunction(query: string) {
const later = () => {
timeout = undefined;
func(query);
};
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
};
}