feat: initialize markr nostr bookmark client

- Add project structure with TypeScript, React, and Vite
- Implement nostr authentication using browser extension (NIP-07)
- Add NIP-51 compliant bookmark fetching and display
- Create minimal UI with login and bookmark components
- Integrate applesauce-core and applesauce-react libraries
- Add responsive styling with dark/light mode support
- Include comprehensive README with setup instructions

This is a minimal MVP for a nostr bookmark client that allows users to
view their bookmarks according to NIP-51 specification.
This commit is contained in:
Gigi
2025-10-02 07:17:07 +02:00
commit 5d53a827e0
11194 changed files with 1827829 additions and 0 deletions

21
node_modules/applesauce-react/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2024 hzrd149
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

35
node_modules/applesauce-react/README.md generated vendored Normal file
View File

@@ -0,0 +1,35 @@
# applesauce-react
React hooks and providers for applesauce
## Installation
```bash
npm install applesauce-react
```
## Example
```tsx
import { EventStore, Models } from "applesauce-core";
import { EventStoreProvider } from "applesauce-react/providers";
import { useEventModel } from "applesauce-react/hooks";
const eventStore = new EventStore();
function UserName({ pubkey }) {
const profile = useEventModel(Models.ProfileModel, [pubkey]);
return <span>{profile.name || "loading..."}</span>;
}
function App() {
return (
<EventStoreProvider eventStore={eventStore}>
<h1>App</h1>
<UserName pubkey="82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2" />
</EventStoreProvider>
);
}
```

View File

@@ -0,0 +1,3 @@
import { Link } from "applesauce-content/nast";
export type LinkRenderer = (url: URL, node: Link) => JSX.Element | false | null;
export declare function buildLinkRenderer(handlers: LinkRenderer[]): import("react").NamedExoticComponent<import("./nast.js").ExtraProps<Link>>;

View File

@@ -0,0 +1,23 @@
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
import { memo, useMemo } from "react";
export function buildLinkRenderer(handlers) {
const LinkRenderer = ({ node }) => {
const content = useMemo(() => {
try {
const url = new URL(node.href);
for (const handler of handlers) {
try {
const content = handler(url, node);
if (content)
return content;
}
catch (e) { }
}
}
catch (error) { }
return null;
}, [node.href, node.value]);
return content || _jsx(_Fragment, { children: node.value });
};
return memo(LinkRenderer);
}

View File

@@ -0,0 +1,2 @@
export * from "./nast.js";
export * from "./build-link-renderer.js";

2
node_modules/applesauce-react/dist/helpers/index.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
export * from "./nast.js";
export * from "./build-link-renderer.js";

12
node_modules/applesauce-react/dist/helpers/nast.d.ts generated vendored Normal file
View File

@@ -0,0 +1,12 @@
import React from "react";
import { Content, ContentMap, Root } from "applesauce-content/nast";
type Component<ComponentProps> = React.FunctionComponent<ComponentProps> | React.ComponentClass;
export type ExtraProps<T extends Content> = {
node: T;
};
export type ComponentMap = Partial<{
[k in keyof ContentMap]: Component<ExtraProps<ContentMap[k]>>;
}>;
/** Render a nostr syntax tree to JSX components */
export declare function renderNast(root: Root, components: ComponentMap): import("react/jsx-runtime").JSX.Element;
export {};

15
node_modules/applesauce-react/dist/helpers/nast.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
/** Render a nostr syntax tree to JSX components */
export function renderNast(root, components) {
const indexes = {};
return (_jsx(_Fragment, { children: root.children.map((node) => {
indexes[node.type] = indexes[node.type] ?? 0;
const index = indexes[node.type];
indexes[node.type]++;
const Component = components[node.type];
if (!Component)
return null;
// @ts-expect-error
return _jsx(Component, { node: node }, node.type + "-" + index);
}) }));
}

12
node_modules/applesauce-react/dist/hooks/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,12 @@
export * from "./use-account-manager.js";
export * from "./use-accounts.js";
export * from "./use-action-hub.js";
export * from "./use-action.js";
export * from "./use-active-account.js";
export * from "./use-event-factory.js";
export * from "./use-event-model.js";
export * from "./use-event-store.js";
export * from "./use-observable-memo.js";
export * from "./use-observable.js";
export * from "./use-render-nast.js";
export * from "./use-rendered-content.js";

12
node_modules/applesauce-react/dist/hooks/index.js generated vendored Normal file
View File

@@ -0,0 +1,12 @@
export * from "./use-account-manager.js";
export * from "./use-accounts.js";
export * from "./use-action-hub.js";
export * from "./use-action.js";
export * from "./use-active-account.js";
export * from "./use-event-factory.js";
export * from "./use-event-model.js";
export * from "./use-event-store.js";
export * from "./use-observable-memo.js";
export * from "./use-observable.js";
export * from "./use-render-nast.js";
export * from "./use-rendered-content.js";

View File

@@ -0,0 +1,4 @@
import { AccountManager } from "applesauce-accounts";
export declare function useAccountManager(): AccountManager;
export declare function useAccountManager(require: false): AccountManager | undefined;
export declare function useAccountManager(require: true): AccountManager;

View File

@@ -0,0 +1,8 @@
import { useContext } from "react";
import { AccountsContext } from "../providers/accounts-provider.js";
export function useAccountManager(require = true) {
const manager = useContext(AccountsContext);
if (!manager && require)
throw new Error("Missing AccountsProvider");
return manager;
}

View File

@@ -0,0 +1,2 @@
import { IAccount } from "applesauce-accounts";
export declare function useAccounts(): IAccount[];

View File

@@ -0,0 +1,6 @@
import { useObservableEagerState } from "observable-hooks";
import { useAccountManager } from "./use-account-manager.js";
export function useAccounts() {
const manager = useAccountManager();
return useObservableEagerState(manager.accounts$);
}

View File

@@ -0,0 +1 @@
export declare function useActionHub(): import("applesauce-actions").ActionHub;

View File

@@ -0,0 +1,8 @@
import { useContext } from "react";
import { ActionsContext } from "../providers/actions-provider.js";
export function useActionHub() {
const hub = useContext(ActionsContext);
if (!hub)
throw new Error("Missing ActionsProvider");
return hub;
}

View File

@@ -0,0 +1,6 @@
import { ActionConstructor } from "applesauce-actions";
export declare function useAction<Args extends Array<any>>(Action: ActionConstructor<Args>, args: Args | undefined): {
loading: boolean;
run: () => Promise<void>;
exec: () => import("rxjs").Observable<import("nostr-tools").Event> | undefined;
};

37
node_modules/applesauce-react/dist/hooks/use-action.js generated vendored Normal file
View File

@@ -0,0 +1,37 @@
import { useCallback, useRef, useState } from "react";
import { finalize } from "rxjs";
import { useActionHub } from "./use-action-hub.js";
export function useAction(Action, args) {
const [loading, setLoading] = useState(false);
const ref = useRef(args);
ref.current = args;
const hub = useActionHub();
const run = useCallback(async () => {
if (args === undefined)
return;
setLoading(true);
try {
await hub.run(Action, ...args);
setLoading(false);
}
catch (error) {
setLoading(false);
throw error;
}
}, [Action]);
const exec = useCallback(() => {
if (args === undefined)
return;
setLoading(true);
try {
return hub.exec(Action, ...args).pipe(finalize(() => {
setLoading(false);
}));
}
catch (error) {
setLoading(false);
throw error;
}
}, [Action]);
return { loading, run, exec };
}

View File

@@ -0,0 +1,2 @@
import { IAccount } from "applesauce-accounts";
export declare function useActiveAccount(): IAccount | undefined;

View File

@@ -0,0 +1,6 @@
import { useObservableEagerState } from "observable-hooks";
import { useAccountManager } from "./use-account-manager.js";
export function useActiveAccount() {
const manager = useAccountManager();
return useObservableEagerState(manager.active$);
}

View File

@@ -0,0 +1,4 @@
import { EventFactory } from "applesauce-factory";
export declare function useEventFactory(require: false): EventFactory | undefined;
export declare function useEventFactory(require: true): EventFactory;
export declare function useEventFactory(): EventFactory;

View File

@@ -0,0 +1,8 @@
import { useContext } from "react";
import { FactoryContext } from "../providers/factory-provider.js";
export function useEventFactory(require = true) {
const factory = useContext(FactoryContext);
if (!require && !factory)
throw new Error("Missing EventFactoryProvider");
return factory;
}

View File

@@ -0,0 +1,3 @@
import { ModelConstructor } from "applesauce-core";
/** Runs and subscribes to a model on the event store */
export declare function useEventModel<T extends unknown, Args extends Array<any>>(factory: ModelConstructor<T, Args>, args?: Args | null): T | undefined;

View File

@@ -0,0 +1,15 @@
import { withImmediateValueOrDefault } from "applesauce-core";
import hash_sum from "hash-sum";
import { of } from "rxjs";
import { useEventStore } from "./use-event-store.js";
import { useObservableEagerMemo } from "./use-observable-memo.js";
/** Runs and subscribes to a model on the event store */
export function useEventModel(factory, args) {
const store = useEventStore();
return useObservableEagerMemo(() => {
if (args)
return store.model(factory, ...args).pipe(withImmediateValueOrDefault(undefined));
else
return of(undefined);
}, [hash_sum(args), store, factory]);
}

View File

@@ -0,0 +1,6 @@
import { IEventStore } from "applesauce-core/event-store";
/**
* Gets the EventStore from a parent {@link EventStoreProvider} component
* If there is none it throws an error
*/
export declare function useEventStore(): IEventStore;

View File

@@ -0,0 +1,12 @@
import { useContext } from "react";
import { EventStoreContext } from "../providers/store-provider.js";
/**
* Gets the EventStore from a parent {@link EventStoreProvider} component
* If there is none it throws an error
*/
export function useEventStore() {
const store = useContext(EventStoreContext);
if (!store)
throw new Error("Missing EventStoreProvider");
return store;
}

View File

@@ -0,0 +1,7 @@
import { BehaviorSubject, Observable } from "rxjs";
/** A hook that recreates an observable when the dependencies change */
export declare function useObservableMemo<T>(factory: () => BehaviorSubject<T>, deps: any[]): T;
export declare function useObservableMemo<T>(factory: () => Observable<T> | undefined, deps: any[]): T | undefined;
/** A hook that recreates a synchronous observable when the dependencies change */
export declare function useObservableEagerMemo<T>(factory: () => Observable<T>, deps: any[]): T;
export declare function useObservableEagerMemo<T>(factory: () => Observable<T> | undefined, deps: any[]): T | undefined;

View File

@@ -0,0 +1,9 @@
import { useObservableEagerState, useObservableState } from "observable-hooks";
import { useMemo } from "react";
import { of } from "rxjs";
export function useObservableMemo(factory, deps) {
return useObservableState(useMemo(() => factory() || of(undefined), deps));
}
export function useObservableEagerMemo(factory, deps) {
return useObservableEagerState(useMemo(() => factory() || of(undefined), deps));
}

View File

@@ -0,0 +1 @@
export * from "observable-hooks";

View File

@@ -0,0 +1,2 @@
// NOTE: re-export hooks from observable-hooks to avoid confusion
export * from "observable-hooks";

View File

@@ -0,0 +1,5 @@
import { Root } from "applesauce-content/nast";
import { ComponentMap } from "../helpers/nast.js";
export { ComponentMap };
/** A hook to get the rendered output of a nostr syntax tree */
export declare function useRenderNast(root: Root | undefined, components: ComponentMap): JSX.Element | null;

View File

@@ -0,0 +1,6 @@
import { useMemo } from "react";
import { renderNast } from "../helpers/nast.js";
/** A hook to get the rendered output of a nostr syntax tree */
export function useRenderNast(root, components) {
return useMemo(() => (root ? renderNast(root, components) : null), [root, Object.keys(components).join("|")]);
}

View File

@@ -0,0 +1,19 @@
import { getParsedContent } from "applesauce-content/text";
import { EventTemplate, NostrEvent } from "nostr-tools";
import { ComponentMap } from "../helpers/nast.js";
import { LinkRenderer } from "../helpers/build-link-renderer.js";
export { ComponentMap };
type Options = {
/** The key to cache the results under, passing null will disable */
cacheKey: symbol | null;
/** Override transformers */
transformers?: Parameters<typeof getParsedContent>[2];
/** If set will use {@link buildLinkRenderer} to render links */
linkRenderers?: LinkRenderer[];
/** Override event content */
content?: string;
/** Maximum length */
maxLength?: number;
};
/** Returns the parsed and render text content for an event */
export declare function useRenderedContent(event: NostrEvent | EventTemplate | string | undefined, components: ComponentMap, opts?: Options): JSX.Element | null;

View File

@@ -0,0 +1,16 @@
import { useMemo } from "react";
import { truncateContent } from "applesauce-content/nast";
import { getParsedContent } from "applesauce-content/text";
import { useRenderNast } from "./use-render-nast.js";
import { buildLinkRenderer } from "../helpers/build-link-renderer.js";
/** Returns the parsed and render text content for an event */
export function useRenderedContent(event, components, opts) {
// if link renderers are set, override the link components
const _components = useMemo(() => (opts?.linkRenderers ? { ...components, link: buildLinkRenderer(opts.linkRenderers) } : components), [opts?.linkRenderers, components]);
// add additional transformers
const nast = useMemo(() => (event ? getParsedContent(event, opts?.content, opts?.transformers, opts?.cacheKey) : undefined), [event, opts?.content, opts?.transformers, opts?.cacheKey]);
let truncated = nast;
if (opts?.maxLength && nast)
truncated = truncateContent(nast, opts.maxLength);
return useRenderNast(truncated, _components);
}

3
node_modules/applesauce-react/dist/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
export * as Helpers from "./helpers/index.js";
export * as Hooks from "./hooks/index.js";
export * from "./providers/index.js";

3
node_modules/applesauce-react/dist/index.js generated vendored Normal file
View File

@@ -0,0 +1,3 @@
export * as Helpers from "./helpers/index.js";
export * as Hooks from "./hooks/index.js";
export * from "./providers/index.js";

View File

@@ -0,0 +1,7 @@
import { PropsWithChildren } from "react";
import { AccountManager } from "applesauce-accounts";
export declare const AccountsContext: import("react").Context<AccountManager<any> | undefined>;
/** Provides an AccountManager to the component tree */
export declare function AccountsProvider({ manager, children }: PropsWithChildren<{
manager?: AccountManager;
}>): import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,7 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext } from "react";
export const AccountsContext = createContext(undefined);
/** Provides an AccountManager to the component tree */
export function AccountsProvider({ manager, children }) {
return _jsx(AccountsContext.Provider, { value: manager, children: children });
}

View File

@@ -0,0 +1,7 @@
import { PropsWithChildren } from "react";
import { ActionHub } from "applesauce-actions";
export declare const ActionsContext: import("react").Context<ActionHub | undefined>;
/** Provides an ActionHub to the component tree */
export declare function ActionsProvider({ actionHub, children }: PropsWithChildren<{
actionHub?: ActionHub;
}>): import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,7 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext } from "react";
export const ActionsContext = createContext(undefined);
/** Provides an ActionHub to the component tree */
export function ActionsProvider({ actionHub, children }) {
return _jsx(ActionsContext.Provider, { value: actionHub, children: children });
}

View File

@@ -0,0 +1,7 @@
import { EventFactory } from "applesauce-factory";
import { PropsWithChildren } from "react";
export declare const FactoryContext: import("react").Context<EventFactory | undefined>;
/** Provides an {@link EventFactory} to the component tree */
export declare function FactoryProvider({ factory, children }: PropsWithChildren<{
factory?: EventFactory;
}>): import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,7 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext } from "react";
export const FactoryContext = createContext(undefined);
/** Provides an {@link EventFactory} to the component tree */
export function FactoryProvider({ factory, children }) {
return _jsx(FactoryContext.Provider, { value: factory, children: children });
}

View File

@@ -0,0 +1,4 @@
export * from "./factory-provider.js";
export * from "./store-provider.js";
export * from "./accounts-provider.js";
export * from "./actions-provider.js";

View File

@@ -0,0 +1,4 @@
export * from "./factory-provider.js";
export * from "./store-provider.js";
export * from "./accounts-provider.js";
export * from "./actions-provider.js";

View File

@@ -0,0 +1,7 @@
import { PropsWithChildren } from "react";
import { IEventStore } from "applesauce-core";
export declare const EventStoreContext: import("react").Context<IEventStore | null>;
/** Provides a EventStore to the component tree */
export declare function EventStoreProvider({ eventStore, children }: PropsWithChildren<{
eventStore: IEventStore;
}>): import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,7 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext } from "react";
export const EventStoreContext = createContext(null);
/** Provides a EventStore to the component tree */
export function EventStoreProvider({ eventStore, children }) {
return _jsx(EventStoreContext.Provider, { value: eventStore, children: children });
}

79
node_modules/applesauce-react/package.json generated vendored Normal file
View File

@@ -0,0 +1,79 @@
{
"name": "applesauce-react",
"version": "3.1.0",
"description": "React hooks for applesauce",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": [
"nostr",
"react",
"applesauce"
],
"author": "hzrd149",
"license": "MIT",
"files": [
"dist",
"applesauce"
],
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./hooks": {
"import": "./dist/hooks/index.js",
"require": "./dist/hooks/index.js",
"types": "./dist/hooks/index.d.ts"
},
"./hooks/*": {
"import": "./dist/hooks/*.js",
"require": "./dist/hooks/*.js",
"types": "./dist/hooks/*.d.ts"
},
"./providers": {
"import": "./dist/providers/index.js",
"require": "./dist/providers/index.js",
"types": "./dist/providers/index.d.ts"
},
"./providers/*": {
"import": "./dist/providers/*.js",
"require": "./dist/providers/*.js",
"types": "./dist/providers/*.d.ts"
},
"./helpers": {
"import": "./dist/helpers/index.js",
"require": "./dist/helpers/index.js",
"types": "./dist/helpers/index.d.ts"
}
},
"dependencies": {
"applesauce-accounts": "^3.1.0",
"applesauce-actions": "^3.1.0",
"applesauce-content": "^3.1.0",
"applesauce-core": "^3.1.0",
"applesauce-factory": "^3.1.0",
"hash-sum": "^2.0.0",
"nostr-tools": "~2.15",
"observable-hooks": "^4.2.4",
"react": "^18.3.1",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@types/hash-sum": "^1.0.2",
"@types/react": "^18.3.18",
"typescript": "^5.8.3",
"vitest": "^3.2.3"
},
"funding": {
"type": "lightning",
"url": "lightning:nostrudel@geyser.fund"
},
"scripts": {
"build": "tsc",
"watch:build": "tsc --watch > /dev/null",
"test": "vitest run --passWithNoTests",
"watch:test": "vitest"
}
}