├── README.md ├── pnpm-workspace.yaml ├── .npmrc ├── apps ├── badges │ ├── app │ │ ├── favicon.ico │ │ ├── twitter-image.png │ │ ├── opengraph-image.png │ │ ├── page.tsx │ │ ├── new │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── a │ │ │ └── [naddr] │ │ │ │ ├── page.tsx │ │ │ │ └── edit │ │ │ │ └── page.tsx │ │ └── p │ │ │ └── [npub] │ │ │ └── page.tsx │ ├── next.config.js │ ├── public │ │ └── badges.png │ ├── ui │ │ ├── const.ts │ │ ├── link.tsx │ │ ├── container.tsx │ │ ├── main.tsx │ │ ├── theme │ │ │ └── button.ts │ │ ├── surface.tsx │ │ ├── avatar.tsx │ │ ├── footer.tsx │ │ ├── grid.tsx │ │ ├── theme.ts │ │ ├── edit.tsx │ │ ├── zap-circle.tsx │ │ ├── color-mode-switch.tsx │ │ ├── badge-award.tsx │ │ ├── layout.tsx │ │ ├── badge-stats.tsx │ │ ├── profile-badge.tsx │ │ ├── address.tsx │ │ ├── badge-icon.tsx │ │ ├── badge-settings.tsx │ │ ├── award-badge.tsx │ │ └── header.tsx │ ├── next-env.d.ts │ ├── .eslintrc.js │ ├── core │ │ ├── worker.ts │ │ └── rarity.ts │ ├── tsconfig.json │ ├── hooks │ │ ├── useNaddr.ts │ │ ├── useAwards.ts │ │ └── useBadges.ts │ └── package.json ├── emojis │ ├── app │ │ ├── favicon.ico │ │ ├── opengraph-image.png │ │ ├── page.tsx │ │ ├── new │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── browse │ │ │ └── page.tsx │ │ ├── utils.ts │ │ ├── a │ │ │ └── [naddr] │ │ │ │ ├── page.tsx │ │ │ │ └── edit │ │ │ │ └── page.tsx │ │ ├── p │ │ │ └── [npub] │ │ │ │ └── page.tsx │ │ └── e │ │ │ └── [nevent] │ │ │ └── page.tsx │ ├── next.config.js │ ├── public │ │ └── logo.png │ ├── next-env.d.ts │ ├── ui │ │ ├── link.tsx │ │ ├── main.tsx │ │ ├── container.tsx │ │ ├── reaction.tsx │ │ ├── pubkey.tsx │ │ ├── footer.tsx │ │ ├── theme.ts │ │ ├── event.tsx │ │ ├── address.tsx │ │ ├── color-mode-toggle.tsx │ │ ├── edit-emoji-set.tsx │ │ ├── app.tsx │ │ ├── layout.tsx │ │ └── header.tsx │ ├── .eslintrc.js │ ├── hooks │ │ └── useEmojiReactions.ts │ ├── tsconfig.json │ └── package.json ├── relays │ ├── next.config.js │ ├── next-env.d.ts │ ├── app │ │ ├── components │ │ │ ├── link.tsx │ │ │ ├── relays-feed.tsx │ │ │ ├── container.tsx │ │ │ ├── main.tsx │ │ │ ├── pubkey.tsx │ │ │ ├── event.tsx │ │ │ ├── footer.tsx │ │ │ ├── relay-favicon.tsx │ │ │ ├── color-mode-toggle.tsx │ │ │ ├── relay-list.tsx │ │ │ ├── relay-settings.tsx │ │ │ ├── relay-metadata.tsx │ │ │ ├── relay-link.tsx │ │ │ ├── relay-icon.tsx │ │ │ ├── layout.tsx │ │ │ ├── relay.tsx │ │ │ ├── relay-set.tsx │ │ │ ├── profile.tsx │ │ │ ├── header.tsx │ │ │ ├── search-feed.tsx │ │ │ └── address.tsx │ │ ├── page.tsx │ │ ├── relays │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── kinds.ts │ │ ├── theme.ts │ │ ├── p │ │ │ └── [npub] │ │ │ │ └── page.tsx │ │ ├── utils.ts │ │ ├── relay │ │ │ └── [nrelay] │ │ │ │ └── page.tsx │ │ ├── a │ │ │ └── [naddr] │ │ │ │ └── page.tsx │ │ ├── e │ │ │ └── [nevent] │ │ │ │ └── page.tsx │ │ └── hooks │ │ │ └── useRelayMetadata.tsx │ ├── .eslintrc.js │ ├── ui │ │ └── const.ts │ ├── tsconfig.json │ ├── public │ │ └── favicon.ico │ └── package.json └── docs │ ├── .eslintrc.cjs │ ├── .storybook │ ├── manager.js │ ├── storybook-theme.js │ ├── preview.jsx │ ├── ndk.js │ └── main.js │ ├── tsconfig.json │ ├── stories │ ├── onboarding.stories.tsx │ ├── user.stories.tsx │ ├── avatar.stories.tsx │ ├── username.stories.tsx │ ├── follow-button.stories.tsx │ ├── avatar-group.stories.tsx │ ├── amount.stories.tsx │ ├── zap-modal.stories.tsx │ ├── zap-button.stories.tsx │ ├── login.stories.tsx │ ├── note.stories.tsx │ └── feed.stories.tsx │ └── package.json ├── packages ├── core │ ├── src │ │ ├── time.ts │ │ ├── icons │ │ │ ├── props.ts │ │ │ ├── index.ts │ │ │ ├── Heart.tsx │ │ │ ├── Bookmark.tsx │ │ │ ├── ChevronDown.tsx │ │ │ ├── ZapCircle.tsx │ │ │ ├── Link.tsx │ │ │ ├── Zap.tsx │ │ │ ├── Close.tsx │ │ │ ├── Reply.tsx │ │ │ ├── User.tsx │ │ │ ├── Dots.tsx │ │ │ ├── Copy.tsx │ │ │ ├── Brackets.tsx │ │ │ ├── Key.tsx │ │ │ ├── Star.tsx │ │ │ └── Repost.tsx │ │ ├── components │ │ │ ├── Placeholder.tsx │ │ │ ├── useAddresses.tsx │ │ │ ├── QrCode.tsx │ │ │ ├── FormattedRelativeTime.tsx │ │ │ ├── Emoji.tsx │ │ │ ├── Blockquote.tsx │ │ │ ├── Address.tsx │ │ │ ├── NEvent.tsx │ │ │ ├── Username.tsx │ │ │ ├── AvatarGroup.tsx │ │ │ ├── Amount.tsx │ │ │ ├── AsyncButton.tsx │ │ │ ├── NAddr.tsx │ │ │ ├── ZapButton.tsx │ │ │ ├── ReactionIcon.tsx │ │ │ ├── UnknownKind.tsx │ │ │ ├── Event.tsx │ │ │ ├── Avatar.tsx │ │ │ ├── LoginMenu.tsx │ │ │ ├── InputCopy.tsx │ │ │ ├── Reaction.tsx │ │ │ ├── User.tsx │ │ │ ├── PubkeyPicker.tsx │ │ │ ├── Note.tsx │ │ │ └── FollowButton.tsx │ │ ├── tags.ts │ │ ├── translations │ │ │ └── es.json │ │ ├── hooks │ │ │ ├── useCopy.ts │ │ │ ├── useAddresses.tsx │ │ │ ├── useAddress.tsx │ │ │ ├── useProfile.ts │ │ │ ├── useProfiles.ts │ │ │ ├── useFeedback.tsx │ │ │ ├── useLatestEvent.tsx │ │ │ ├── useEvent.ts │ │ │ ├── useRates.ts │ │ │ ├── useEvents.ts │ │ │ └── useReactions.ts │ │ ├── nostr │ │ │ ├── kinds.tsx │ │ │ └── emoji.tsx │ │ ├── i18n.ts │ │ ├── money.ts │ │ ├── theme │ │ │ ├── components │ │ │ │ ├── link.ts │ │ │ │ └── card.ts │ │ │ └── index.ts │ │ ├── filter.ts │ │ ├── utils.ts │ │ ├── format.ts │ │ ├── types.ts │ │ ├── state.ts │ │ └── index.tsx │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── index.d.ts │ └── package.json ├── eslint-config │ ├── README.md │ ├── package.json │ ├── next.js │ ├── library.js │ ├── storybook.js │ └── react.js └── typescript-config │ ├── package.json │ ├── nextjs.json │ ├── react-app.json │ ├── react-library.json │ └── base.json ├── .vscode └── settings.json ├── .gitignore ├── .changeset ├── config.json └── README.md ├── turbo.json ├── package.json ├── LICENSE └── .github └── workflows └── release.yml /README.md: -------------------------------------------------------------------------------- 1 | # ngine 2 | 3 | Work in progress, docs coming soon. 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | public-hoist-pattern[]=*storybook* 3 | -------------------------------------------------------------------------------- /apps/badges/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verbiricha/ngine/HEAD/apps/badges/app/favicon.ico -------------------------------------------------------------------------------- /apps/badges/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | module.exports = { 4 | } 5 | -------------------------------------------------------------------------------- /apps/emojis/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verbiricha/ngine/HEAD/apps/emojis/app/favicon.ico -------------------------------------------------------------------------------- /apps/emojis/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | module.exports = { 4 | } 5 | -------------------------------------------------------------------------------- /apps/emojis/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verbiricha/ngine/HEAD/apps/emojis/public/logo.png -------------------------------------------------------------------------------- /packages/core/src/time.ts: -------------------------------------------------------------------------------- 1 | export function unixNow() { 2 | return Math.round(Date.now() / 1000); 3 | } 4 | -------------------------------------------------------------------------------- /apps/badges/public/badges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verbiricha/ngine/HEAD/apps/badges/public/badges.png -------------------------------------------------------------------------------- /apps/badges/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verbiricha/ngine/HEAD/apps/badges/app/twitter-image.png -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/badges/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verbiricha/ngine/HEAD/apps/badges/app/opengraph-image.png -------------------------------------------------------------------------------- /apps/emojis/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verbiricha/ngine/HEAD/apps/emojis/app/opengraph-image.png -------------------------------------------------------------------------------- /apps/relays/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | transpilePackages: ["@ngine/core"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/docs/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | extends: ["@repo/eslint-config/storybook.js"], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | extends: ["@repo/eslint-config/react.js"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/badges/ui/const.ts: -------------------------------------------------------------------------------- 1 | export const gradient = 2 | "linear-gradient(90deg, #B10BA5 0%, #CB5A7B 50%, #E29F56 100%)"; 3 | export const maxWidth = "72rem"; 4 | -------------------------------------------------------------------------------- /apps/docs/.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/addons" 2 | import theme from "./storybook-theme" 3 | 4 | addons.setConfig({ 5 | theme, 6 | }) 7 | -------------------------------------------------------------------------------- /packages/core/src/icons/props.ts: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export interface IconProps extends SVGProps { 4 | ref?: any; 5 | } 6 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-app.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .turbo 4 | *.log 5 | .next 6 | dist 7 | dist-ssr 8 | *.local 9 | .env 10 | .cache 11 | server/dist 12 | public/dist 13 | storybook-static/ 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /packages/core/src/components/Placeholder.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@chakra-ui/react"; 2 | 3 | export default function Placeholder() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/tags.ts: -------------------------------------------------------------------------------- 1 | import { NDKEvent } from "@nostr-dev-kit/ndk"; 2 | 3 | export function tagValues(ev: NDKEvent, tag: string): string[] { 4 | return ev.tags.filter((t) => t[0] === tag).map((t) => t[1]); 5 | } 6 | -------------------------------------------------------------------------------- /apps/emojis/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "@ui/layout"; 2 | import Home from "@ui/home"; 3 | 4 | export default function Page() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/docs/.storybook/storybook-theme.js: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming" 2 | 3 | export default create({ 4 | base: "light", 5 | brandTitle: "ngine", 6 | //brandUrl: "https://chakra-ui.com", 7 | //brandImage, 8 | }) 9 | -------------------------------------------------------------------------------- /apps/badges/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/badges/ui/link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from "next/link"; 2 | import { Link as BaseLink, LinkProps } from "@chakra-ui/react"; 3 | 4 | export default function Link(props: LinkProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /apps/emojis/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/emojis/ui/link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from "next/link"; 2 | import { Link as BaseLink, LinkProps } from "@chakra-ui/react"; 3 | 4 | export default function Link(props: LinkProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /apps/relays/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/badges/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Home from "@ui/home"; 3 | import Layout from "@ui/layout"; 4 | 5 | export default function HomePage() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/relays/app/components/link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from "next/link"; 2 | import { Link as BaseLink, LinkProps } from "@chakra-ui/react"; 3 | 4 | export default function Link(props: LinkProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /apps/relays/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "./components/layout"; 2 | import Relays from "./components/relays"; 3 | 4 | export default function Page() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/components/useAddresses.tsx: -------------------------------------------------------------------------------- 1 | import useEvents from "./useEvents"; 2 | import { addressesToFilter } from "../filter"; 3 | 4 | export default function useAddresses(addresses: string[]) { 5 | return useEvents(addressesToFilter(addresses)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/emojis/app/new/page.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "@ui/layout"; 2 | import NewEmojiSet from "@ui/new-emoji-set"; 3 | 4 | export default function NewEmojiSetPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/badges/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/badges/app/new/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import NewBadge from "@ui/new-badge"; 4 | import Layout from "@ui/layout"; 5 | 6 | export default function HomePage() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/emojis/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/relays/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core/src/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "ngine.extension-login": "Login con extensión", 3 | "ngine.follow": "Seguir", 4 | "ngine.read-less": "Ocultar historia", 5 | "ngine.read-more": "Leer historia completa", 6 | "ngine.unfollow": "Dejar de seguir" 7 | } 8 | -------------------------------------------------------------------------------- /apps/docs/.storybook/preview.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { NgineProvider } from "@ngine/core"; 3 | import ndk from "./ndk" 4 | 5 | export const decorators = [ 6 | (Story) => 7 | ] 8 | -------------------------------------------------------------------------------- /apps/relays/ui/const.ts: -------------------------------------------------------------------------------- 1 | import { NDKKind } from "@nostr-dev-kit/ndk"; 2 | 3 | export const maxWidth = "48em"; 4 | 5 | export const NOSTR_WATCH_MONITOR = 6 | "abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832"; 7 | export const RELAY_METADATA = 30_166 as NDKKind; 8 | -------------------------------------------------------------------------------- /packages/core/src/components/QrCode.tsx: -------------------------------------------------------------------------------- 1 | import { QRCodeSVG } from "qrcode.react"; 2 | 3 | export interface QrCodeProps { 4 | data: string; 5 | } 6 | 7 | export default function QrCode({ data }: QrCodeProps) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useCopy.ts: -------------------------------------------------------------------------------- 1 | export default function useCopy() { 2 | const copy = async (text: string) => { 3 | if (typeof navigator === "undefined") { 4 | return; 5 | } 6 | 7 | await navigator.clipboard.writeText(text); 8 | }; 9 | 10 | return copy; 11 | } 12 | -------------------------------------------------------------------------------- /apps/relays/app/relays/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Layout from "../components/layout"; 4 | import MyRelays from "../components/my-relays"; 5 | 6 | export default function MyRelaysPage() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "devDependencies": { 10 | "@formatjs/ts-transformer": "^3.13.9" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/nostr/kinds.tsx: -------------------------------------------------------------------------------- 1 | import { NDKKind } from "@nostr-dev-kit/ndk"; 2 | 3 | export const REPOSTS = [NDKKind.Repost, NDKKind.GenericRepost]; 4 | 5 | export const BOOKMARKS = [ 6 | NDKKind.BookmarkList, 7 | NDKKind.CategorizedBookmarkList, 8 | NDKKind.RelayList, 9 | NDKKind.EmojiList, 10 | ]; 11 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "updateInternalDependencies": "patch", 9 | "ignore": ["@acme/docs"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import en from "./translations/en.json"; 2 | import es from "./translations/es.json"; 3 | 4 | export function getMessages(locale?: string) { 5 | if (!locale) { 6 | return en; 7 | } 8 | 9 | if (locale.startsWith("es")) { 10 | return es; 11 | } 12 | 13 | return en; 14 | } 15 | -------------------------------------------------------------------------------- /apps/relays/app/components/relays-feed.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Feed } from "@ngine/core"; 4 | 5 | import { kinds, components } from "../kinds"; 6 | 7 | export default function RelaysFeed({ relays }: { relays: string[] }) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /apps/emojis/ui/main.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "@chakra-ui/react"; 2 | 3 | export default function Main(props: StackProps) { 4 | return ( 5 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/relays/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Nostrrr", 3 | description: "A nostr relay explorer", 4 | }; 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/badges/ui/container.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, FlexProps } from "@chakra-ui/react"; 2 | 3 | export default function Container(props: FlexProps) { 4 | return ( 5 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/badges/ui/main.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "@chakra-ui/react"; 2 | 3 | import { maxWidth } from "./const"; 4 | 5 | export default function Main(props: StackProps) { 6 | return ( 7 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/emojis/ui/container.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, FlexProps } from "@chakra-ui/react"; 2 | 3 | export default function Container(props: FlexProps) { 4 | return ( 5 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Section { 2 | name: string; 3 | letters: string; 4 | value: Record; 5 | } 6 | 7 | interface DecodedLightningInvoice { 8 | paymentRequest: string; 9 | sections: Section[]; 10 | } 11 | 12 | declare module "light-bolt11-decoder" { 13 | export function decode(pr: string): DecodedLightningInvoice; 14 | } 15 | -------------------------------------------------------------------------------- /apps/relays/app/components/container.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, FlexProps } from "@chakra-ui/react"; 2 | 3 | export default function Container(props: FlexProps) { 4 | return ( 5 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/components/FormattedRelativeTime.tsx: -------------------------------------------------------------------------------- 1 | import { formatRelativeTime } from "../format"; 2 | 3 | interface FormattedRelativeTimeProps { 4 | timestamp: number; 5 | } 6 | 7 | // todo: short version 8 | export default function FormattedRelativeTime({ 9 | timestamp, 10 | }: FormattedRelativeTimeProps) { 11 | return <>{formatRelativeTime(timestamp)}; 12 | } 13 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "outputs": ["dist/**", "storybook-static/**"], 6 | "dependsOn": ["^build"] 7 | }, 8 | "lint": {}, 9 | "dev": { 10 | "cache": false, 11 | "persistent": true 12 | }, 13 | "clean": { 14 | "cache": false 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "allowJs": true, 10 | "jsx": "preserve", 11 | "noEmit": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/relays/app/components/main.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "@chakra-ui/react"; 2 | import { maxWidth } from "@ui/const"; 3 | 4 | export default function Main(props: StackProps) { 5 | return ( 6 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/badges/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | metadataBase: new URL("https://badges.page"), 3 | title: "Badges", 4 | description: "Create, collect and award badges", 5 | }; 6 | 7 | export default function RootLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/money.ts: -------------------------------------------------------------------------------- 1 | import { Rates } from "./types"; 2 | 3 | export function convertSatsToFiat(amt: string, rates: Rates): string { 4 | const inBtc = Number(amt) / 1e8; 5 | return String((rates.ask * inBtc).toFixed(2)); 6 | } 7 | 8 | export function convertFiatToSats(amt: string, rates: Rates): string { 9 | const inFiat = Number(amt); 10 | return String(((inFiat / rates.ask) * 1e8).toFixed(0)); 11 | } 12 | -------------------------------------------------------------------------------- /apps/emojis/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | metadataBase: new URL("https://emojito.meme"), 3 | title: "emojito", 4 | description: "Stir up your reactions with custom emoji", 5 | }; 6 | 7 | export default function RootLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/components/Emoji.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, Image, ImageProps } from "@chakra-ui/react"; 2 | 3 | interface EmojiProps extends ImageProps { 4 | alt: string; 5 | } 6 | 7 | export default function Emoji({ alt, boxSize = 4, ...props }: EmojiProps) { 8 | return ( 9 | 10 | {alt} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/relays/app/kinds.ts: -------------------------------------------------------------------------------- 1 | import { NDKKind } from "@nostr-dev-kit/ndk"; 2 | 3 | import RelayList from "./components/relay-list"; 4 | import RelaySet from "./components/relay-set"; 5 | 6 | export const kinds = [ 7 | NDKKind.Text, 8 | NDKKind.Article, 9 | NDKKind.RelayList, 10 | NDKKind.RelaySet, 11 | ]; 12 | 13 | export const components = { 14 | [NDKKind.RelayList]: RelayList, 15 | [NDKKind.RelaySet]: RelaySet, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useAddresses.tsx: -------------------------------------------------------------------------------- 1 | import { NDKSubscriptionOptions } from "@nostr-dev-kit/ndk"; 2 | import useEvents, { SubscriptionOptions } from "./useEvents"; 3 | import { addressesToFilter } from "../filter"; 4 | 5 | export default function useAddresses( 6 | addresses: string[], 7 | opts?: SubscriptionOptions, 8 | relays?: string[], 9 | ) { 10 | return useEvents(addressesToFilter(addresses), opts, relays); 11 | } 12 | -------------------------------------------------------------------------------- /apps/badges/app/a/[naddr]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Flex } from "@chakra-ui/react"; 4 | import Layout from "@ui/layout"; 5 | import Address from "@ui/address"; 6 | 7 | export default function BadgePage({ params }: { params: { naddr: string } }) { 8 | return ( 9 | 10 | 11 |
12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/docs/stories/onboarding.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Onboarding } from "@ngine/core"; 3 | 4 | const meta: Meta = { 5 | title: "Components / Onboarding", 6 | component: Onboarding, 7 | }; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Primary: Story = { 14 | name: "Onboarding", 15 | args: {}, 16 | }; 17 | -------------------------------------------------------------------------------- /apps/emojis/ui/reaction.tsx: -------------------------------------------------------------------------------- 1 | import { HStack } from "@chakra-ui/react"; 2 | import { EventProps, ReactionIcon, Avatar } from "@ngine/core"; 3 | 4 | export default function Reaction({ event }: EventProps) { 5 | const p = event.tagValue("p"); 6 | return ( 7 | 8 | 9 | 10 | {p && } 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/badges/app/a/[naddr]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Flex } from "@chakra-ui/react"; 4 | import Layout from "@ui/layout"; 5 | import Edit from "@ui/edit"; 6 | 7 | export default function EditBadgePage({ 8 | params, 9 | }: { 10 | params: { naddr: string }; 11 | }) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/badges/ui/theme/button.ts: -------------------------------------------------------------------------------- 1 | import { defineStyle, defineStyleConfig } from "@chakra-ui/react"; 2 | import type { StyleConfig } from "@chakra-ui/styled-system"; 3 | 4 | const gradient = defineStyle({ 5 | color: "white", 6 | borderRadius: "11000px", 7 | background: "linear-gradient(90deg, #B10BA5 0%, #CB5A7B 50%, #E29F56 100%)", 8 | }); 9 | 10 | // @ts-ignore 11 | export const buttonTheme: StyleConfig = defineStyleConfig({ 12 | variants: { gradient }, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/core/src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Brackets"; 2 | export * from "./ZapCircle"; 3 | export * from "./Link"; 4 | export * from "./Zap"; 5 | export * from "./User"; 6 | export * from "./Copy"; 7 | export * from "./ChevronDown"; 8 | export * from "./Key"; 9 | export * from "./Heart"; 10 | export * from "./Reply"; 11 | export * from "./Repost"; 12 | export * from "./Dots"; 13 | export * from "./Star"; 14 | export * from "./Bookmark"; 15 | export * from "./Close"; 16 | -------------------------------------------------------------------------------- /apps/badges/core/worker.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | import { NostrEvent, nip13 } from "nostr-tools"; 3 | 4 | addEventListener( 5 | "message", 6 | (e: MessageEvent<{ event: NostrEvent; difficulty: number }>) => { 7 | if (!e) return; 8 | const { event, difficulty } = e.data; 9 | console.log("FINDING POW FOR EV", event, difficulty); 10 | 11 | const ev = nip13.minePow(event, difficulty); 12 | 13 | postMessage(ev); 14 | }, 15 | ); 16 | -------------------------------------------------------------------------------- /packages/core/src/components/Blockquote.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Text } from "@chakra-ui/react"; 3 | 4 | export default function Blockquote({ children }: { children: ReactNode }) { 5 | return ( 6 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/docs/stories/user.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { User } from "@ngine/core"; 3 | 4 | const meta: Meta = { 5 | title: "Components / User", 6 | component: User, 7 | }; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Primary: Story = { 14 | name: "User", 15 | args: { 16 | pubkey: "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/core/src/theme/components/link.ts: -------------------------------------------------------------------------------- 1 | import { defineStyle, defineStyleConfig } from "@chakra-ui/react"; 2 | import { MultiStyleConfig } from "@chakra-ui/styled-system"; 3 | 4 | const brand = defineStyle({ 5 | fontWeight: 500, 6 | color: "brand.500", 7 | _dark: { 8 | color: "brand.300", 9 | }, 10 | 11 | _hover: { 12 | textDecoration: "none", 13 | }, 14 | }); 15 | 16 | export const linkTheme: MultiStyleConfig = defineStyleConfig({ 17 | variants: { brand }, 18 | }); 19 | -------------------------------------------------------------------------------- /apps/docs/stories/avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Avatar } from "@ngine/core"; 3 | 4 | const meta: Meta = { 5 | title: "Components / Avatar", 6 | component: Avatar, 7 | }; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Primary: Story = { 14 | name: "Avatar", 15 | args: { 16 | pubkey: "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/typescript-config/react-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "declaration": false, 8 | "declarationMap": false, 9 | "incremental": true, 10 | "jsx": "preserve", 11 | "lib": ["dom", "dom.iterable", "esnext"], 12 | "module": "esnext", 13 | "noEmit": true, 14 | "resolveJsonModule": true, 15 | "target": "es5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/docs/stories/username.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Username } from "@ngine/core"; 3 | 4 | const meta: Meta = { 5 | title: "Components / Username", 6 | component: Username, 7 | }; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Primary: Story = { 14 | name: "Username", 15 | args: { 16 | pubkey: "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "library.js", 7 | "react.js", 8 | "storybook.j" 9 | ], 10 | "devDependencies": { 11 | "@vercel/style-guide": "^5.0.0", 12 | "eslint-config-turbo": "^1.10.12", 13 | "eslint-plugin-formatjs": "^4.11.3", 14 | "eslint-plugin-mdx": "^2.2.0", 15 | "eslint-plugin-only-warn": "^1.1.0", 16 | "eslint-plugin-storybook": "^0.6.13" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/badges/ui/surface.tsx: -------------------------------------------------------------------------------- 1 | import { useColorModeValue, Stack, StackProps } from "@chakra-ui/react"; 2 | 3 | export default function Surface({ children, ...props }: StackProps) { 4 | const bg = useColorModeValue("gray.50", "#1D1D1D"); 5 | return ( 6 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/docs/stories/follow-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { FollowButton } from "@ngine/core"; 3 | 4 | const meta: Meta = { 5 | title: "Components / Follow", 6 | component: FollowButton, 7 | }; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Primary: Story = { 14 | name: "Follow", 15 | args: { 16 | pubkey: "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/core/src/components/Address.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | import Event from "./Event"; 4 | import Placeholder from "./Placeholder"; 5 | import { EventProps } from "../types"; 6 | import useAddress from "../hooks/useAddress"; 7 | 8 | interface AddressProps extends Omit { 9 | address: string; 10 | } 11 | 12 | export default function Address({ address, ...props }: AddressProps) { 13 | const event = useAddress(address); 14 | return event ? : ; 15 | } 16 | -------------------------------------------------------------------------------- /apps/badges/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Box, AvatarProps as BaseAvatarProps } from "@chakra-ui/react"; 2 | import { Avatar as NostrAvatar } from "@ngine/core"; 3 | 4 | import { gradient } from "./const"; 5 | 6 | interface AvatarProps extends BaseAvatarProps { 7 | p: number; 8 | pubkey: string; 9 | } 10 | 11 | export default function Avatar({ p = 1, pubkey, ...props }: AvatarProps) { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/emojis/app/browse/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { NDKKind } from "@nostr-dev-kit/ndk"; 4 | import { Feed } from "@ngine/core"; 5 | 6 | import Layout from "@ui/layout"; 7 | import EmojiSet from "@ui/emoji-set"; 8 | 9 | export default function Browse() { 10 | return ( 11 | 12 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/emojis/hooks/useEmojiReactions.ts: -------------------------------------------------------------------------------- 1 | import { NDKKind } from "@nostr-dev-kit/ndk"; 2 | import { useMemo } from "react"; 3 | import { useEvents } from "@ngine/core"; 4 | 5 | export default function useEmojiReactions() { 6 | const { events } = useEvents( 7 | { 8 | kinds: [NDKKind.Reaction], 9 | limit: 1_00, 10 | }, 11 | { 12 | closeOnEose: false, 13 | }, 14 | ); 15 | const reactions = useMemo(() => { 16 | return events.filter((e) => e.tags.find((t) => t[0] === "emoji")); 17 | }, [events]); 18 | return reactions; 19 | } 20 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["dom", "ES2019"], 8 | "module": "ESNext", 9 | "target": "es6", 10 | "plugins": [ 11 | { 12 | "transform": "@formatjs/ts-transformer", 13 | "import": "transform", 14 | "type": "config", 15 | "overrideIdFn": "[sha512:contenthash:base64:6]", 16 | "ast": true 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/docs/.storybook/ndk.js: -------------------------------------------------------------------------------- 1 | import NDK from "@nostr-dev-kit/ndk" 2 | import NDKCacheAdapterDexie from "@nostr-dev-kit/ndk-cache-dexie"; 3 | 4 | const cacheAdapter = new NDKCacheAdapterDexie({ dbName: "storybook" }); 5 | const ndk = new NDK({ 6 | explicitRelayUrls: ["wss://nos.lol", "wss://relay.nostr.band"], 7 | outboxRelayUrls: ["wss://purplepag.es"], 8 | enableOutboxModel: true, 9 | cacheAdapter, 10 | }) 11 | 12 | let isConnected = false; 13 | 14 | if (!isConnected) { 15 | ndk.connect().then(function(){ 16 | isConnected = true; 17 | }); 18 | } 19 | 20 | export default ndk 21 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useAddress.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | import useEvent from "./useEvent"; 4 | 5 | export default function useAddress(address: string, relays?: string[]) { 6 | const { kind, pubkey, identifier } = useMemo(() => { 7 | const [k, pubkey, identifier] = address.split(":"); 8 | return { kind: Number(k), pubkey, identifier }; 9 | }, [address]); 10 | const event = useEvent( 11 | { 12 | kinds: [kind], 13 | authors: [pubkey], 14 | "#d": [identifier], 15 | }, 16 | {}, 17 | relays, 18 | ); 19 | return event; 20 | } 21 | -------------------------------------------------------------------------------- /apps/emojis/ui/pubkey.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FormattedMessage } from "react-intl"; 4 | import { Alert, AlertIcon } from "@chakra-ui/react"; 5 | 6 | import Profile from "./profile"; 7 | 8 | export default function Pubkey({ pubkey }: { pubkey?: string }) { 9 | return pubkey ? ( 10 | 11 | ) : ( 12 | 13 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/components/NEvent.tsx: -------------------------------------------------------------------------------- 1 | import Event from "./Event"; 2 | import Placeholder from "./Placeholder"; 3 | import { EventProps, Components } from "../types"; 4 | import useEvent from "../hooks/useEvent"; 5 | 6 | interface NEventProps extends Omit { 7 | id: string; 8 | relays?: string[]; 9 | } 10 | 11 | export default function NEvent({ id, relays, ...props }: NEventProps) { 12 | const event = useEvent( 13 | { 14 | ids: [id], 15 | }, 16 | {}, 17 | relays, 18 | ); 19 | return event ? : ; 20 | } 21 | -------------------------------------------------------------------------------- /apps/relays/app/components/pubkey.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FormattedMessage } from "react-intl"; 4 | import { Alert, AlertIcon } from "@chakra-ui/react"; 5 | 6 | import Profile from "./profile"; 7 | 8 | export default function Pubkey({ pubkey }: { pubkey?: string }) { 9 | return pubkey ? ( 10 | 11 | ) : ( 12 | 13 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "turbo run build", 5 | "dev": "turbo run dev", 6 | "lint": "turbo run lint", 7 | "clean": "turbo run clean && rm -rf node_modules", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 9 | "changeset": "changeset", 10 | "version-packages": "changeset version", 11 | "release": "turbo run build --filter=docs^... && changeset publish" 12 | }, 13 | "devDependencies": { 14 | "@changesets/cli": "^2.25.2", 15 | "prettier": "^3.0.3", 16 | "turbo": "latest" 17 | }, 18 | "packageManager": "pnpm@8.9.0" 19 | } 20 | -------------------------------------------------------------------------------- /apps/badges/ui/footer.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, Text } from "@chakra-ui/react"; 2 | import Link from "./link"; 3 | 4 | export default function Footer(props: StackProps) { 5 | return ( 6 | 15 | 16 | Powered by{" "} 17 | 18 | ngine 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/emojis/ui/footer.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, Text } from "@chakra-ui/react"; 2 | import Link from "./link"; 3 | 4 | export default function Footer(props: StackProps) { 5 | return ( 6 | 15 | 16 | Powered by{" "} 17 | 18 | ngine 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/components/Username.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "@chakra-ui/react"; 2 | import type { TextProps } from "@chakra-ui/react"; 3 | 4 | import useProfile from "../hooks/useProfile"; 5 | 6 | function shortenPubkey(pk: string) { 7 | return `${pk.slice(0, 8)}`; 8 | } 9 | 10 | interface UsernameProps extends TextProps { 11 | pubkey: string; 12 | } 13 | 14 | export default function Username({ pubkey, ...rest }: UsernameProps) { 15 | const profile = useProfile(pubkey); 16 | return ( 17 | 18 | {profile?.display_name || profile?.name || shortenPubkey(pubkey)} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/badges/ui/grid.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { SimpleGrid, SimpleGridProps } from "@chakra-ui/react"; 3 | 4 | interface GridProps extends SimpleGridProps { 5 | maxCol?: number; 6 | } 7 | 8 | export default function Grid({ 9 | maxCol = 3, 10 | spacing = 3, 11 | children, 12 | ...props 13 | }: GridProps) { 14 | return ( 15 | 25 | {children} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/relays/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ], 9 | "lib": [ 10 | "dom", 11 | "dom.iterable", 12 | "esnext" 13 | ], 14 | "incremental": true, 15 | "resolveJsonModule": true, 16 | "baseUrl": "./", 17 | "paths": { 18 | "@ui/*": ["ui/*"], 19 | } 20 | }, 21 | "include": [ 22 | "next-env.d.ts", 23 | "next.config.js", 24 | "**/*.ts", 25 | "**/*.tsx", 26 | ".next/types/**/*.ts" 27 | ], 28 | "exclude": [ 29 | "node_modules" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/components/AvatarGroup.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AvatarGroup as BaseAvatarGroup, 3 | AvatarGroupProps as BaseAvatarGroupProps, 4 | } from "@chakra-ui/react"; 5 | 6 | import Avatar from "./Avatar"; 7 | 8 | interface AvatarGroupProps extends Omit { 9 | pubkeys: string[]; 10 | } 11 | 12 | // todo: tweak spacing and bg for + part 13 | export default function AvatarGroup({ pubkeys, ...rest }: AvatarGroupProps) { 14 | return ( 15 | 16 | {pubkeys.map((pk) => ( 17 | 18 | ))} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/components/Amount.tsx: -------------------------------------------------------------------------------- 1 | import { useExchangeRate, useCurrency } from "../state"; 2 | import { formatSats, formatSatAmount } from "../format"; 3 | import type { Currency } from "../types"; 4 | 5 | interface AmountProps { 6 | amount: number; 7 | currency?: Currency; 8 | } 9 | 10 | export default function Amount({ amount, currency }: AmountProps) { 11 | const defaultCurrency = useCurrency(); 12 | const selectedCurrency = currency || defaultCurrency; 13 | const rates = useExchangeRate(selectedCurrency); 14 | 15 | if (!rates) { 16 | return <>{formatSats(amount)}; 17 | } 18 | 19 | return <>{formatSatAmount(amount, selectedCurrency, rates)}; 20 | } 21 | -------------------------------------------------------------------------------- /apps/emojis/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ], 9 | "lib": [ 10 | "dom", 11 | "dom.iterable", 12 | "esnext" 13 | ], 14 | "incremental": true, 15 | "resolveJsonModule": true, 16 | "baseUrl": "./", 17 | "paths": { 18 | "@ui/*": ["ui/*"], 19 | "@hooks/*": ["hooks/*"] 20 | }, 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "next.config.js", 25 | "**/*.ts", 26 | "**/*.tsx", 27 | ".next/types/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/components/AsyncButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Button, ButtonProps } from "@chakra-ui/react"; 3 | 4 | interface AsyncButtonProps extends ButtonProps { 5 | onClick: () => Promise; 6 | } 7 | 8 | export default function AsyncButton({ onClick, ...props }: AsyncButtonProps) { 9 | const [isBusy, setIsBusy] = useState(false); 10 | async function asyncAction() { 11 | try { 12 | setIsBusy(true); 13 | await onClick(); 14 | } catch (error) { 15 | console.error(error); 16 | } finally { 17 | setIsBusy(false); 18 | } 19 | } 20 | return 21 | 26 | 27 | ); 28 | }, 29 | args: {}, 30 | }; 31 | -------------------------------------------------------------------------------- /apps/emojis/app/utils.ts: -------------------------------------------------------------------------------- 1 | import { Relay, Tag } from "@ngine/core"; 2 | 3 | export function encodeRelayURL(url: string): string { 4 | url = url.trim(); 5 | if (url.startsWith("wss://")) { 6 | url = url.slice(6); 7 | } 8 | return encodeURIComponent(url); 9 | } 10 | 11 | export function relayToTag(r: Relay): Tag { 12 | if (r.read && !r.write) { 13 | return ["r", r.url, "read"]; 14 | } 15 | if (r.write && !r.read) { 16 | return ["r", r.url, "write"]; 17 | } 18 | return ["r", r.url]; 19 | } 20 | 21 | export function tagToRelay(t: Tag): Relay { 22 | const url = t[1].replace(/\/$/, ""); 23 | 24 | if (t[2] === "read") { 25 | return { url, read: true, write: false }; 26 | } 27 | 28 | if (t[2] === "write") { 29 | return { url, read: false, write: true }; 30 | } 31 | 32 | return { url, read: true, write: true }; 33 | } 34 | -------------------------------------------------------------------------------- /apps/relays/app/p/[npub]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { nip19 } from "nostr-tools"; 5 | 6 | import Layout from "../../components/layout"; 7 | import Pubkey from "../../components/pubkey"; 8 | 9 | export default function ProfilePage({ params }: { params: { npub: string } }) { 10 | // todo: not found msg 11 | const { npub } = params; 12 | const pubkey = useMemo(() => { 13 | try { 14 | const decoded = nip19.decode(npub); 15 | if (decoded.type === "npub") { 16 | return decoded.data; 17 | } 18 | if (decoded.type === "nprofile") { 19 | return decoded.data.pubkey; 20 | } 21 | } catch (error) { 22 | console.error(error); 23 | } 24 | }, [npub]); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/relays/app/utils.ts: -------------------------------------------------------------------------------- 1 | import { Relay, Tag } from "@ngine/core"; 2 | 3 | export function encodeRelayURL(url: string): string { 4 | url = url.trim(); 5 | if (url.startsWith("wss://")) { 6 | url = url.slice(6); 7 | } 8 | return encodeURIComponent(url); 9 | } 10 | 11 | export function relayToTag(r: Relay): Tag { 12 | if (r.read && !r.write) { 13 | return ["r", r.url, "read"]; 14 | } 15 | if (r.write && !r.read) { 16 | return ["r", r.url, "write"]; 17 | } 18 | return ["r", r.url]; 19 | } 20 | 21 | export function tagToRelay(t: Tag): Relay { 22 | const url = t[1].replace(/\/$/, ""); 23 | 24 | if (t[2] === "read") { 25 | return { url, read: true, write: false }; 26 | } 27 | 28 | if (t[2] === "write") { 29 | return { url, read: false, write: true }; 30 | } 31 | 32 | return { url, read: true, write: true }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/icons/Zap.tsx: -------------------------------------------------------------------------------- 1 | import type { IconProps } from "./props"; 2 | 3 | export function Zap(props: IconProps) { 4 | return ( 5 | 11 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useProfiles.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { 3 | NDKUserProfile, 4 | NDKSubscriptionCacheUsage, 5 | NDKKind, 6 | } from "@nostr-dev-kit/ndk"; 7 | 8 | import { useNDK } from "../context"; 9 | 10 | export default function useProfiles(pubkeys: string[]) { 11 | const ndk = useNDK(); 12 | const [profiles, setProfiles] = useState([]); 13 | 14 | useEffect(() => { 15 | ndk 16 | .fetchEvents( 17 | { 18 | kinds: [NDKKind.Metadata], 19 | authors: pubkeys, 20 | }, 21 | { 22 | cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, 23 | }, 24 | ) 25 | .then((profileSet) => { 26 | return setProfiles([...profileSet].map((ev) => JSON.parse(ev.content))); 27 | }); 28 | }, [pubkeys]); 29 | 30 | return profiles; 31 | } 32 | -------------------------------------------------------------------------------- /apps/relays/app/components/relay-favicon.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarProps, Tooltip, Icon } from "@chakra-ui/react"; 2 | 3 | import RelayIcon from "./relay-icon"; 4 | import { useRelayMetadata } from "../hooks/useRelayMetadata"; 5 | 6 | interface RelayFaviconProps extends AvatarProps { 7 | url: string; 8 | } 9 | 10 | export default function RelayFavicon({ url, ...rest }: RelayFaviconProps) { 11 | const { data: metadata } = useRelayMetadata(url); 12 | const domain = url 13 | .replace("wss://", "https://") 14 | .replace("ws://", "http://") 15 | .replace("relay.damus", "damus") 16 | .replace(/\/$/, ""); 17 | return ( 18 | 19 | } 23 | {...rest} 24 | /> 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/eslint-config/library.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * typescript packages. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "@vercel/style-guide/eslint/node", 17 | "@vercel/style-guide/eslint/typescript", 18 | ].map(require.resolve), 19 | parserOptions: { 20 | project, 21 | }, 22 | plugins: ["only-warn"], 23 | globals: { 24 | React: true, 25 | JSX: true, 26 | }, 27 | settings: { 28 | "import/resolver": { 29 | typescript: { 30 | project, 31 | }, 32 | }, 33 | }, 34 | ignorePatterns: ["node_modules/", "dist/"], 35 | }; 36 | -------------------------------------------------------------------------------- /apps/emojis/ui/color-mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useColorMode, Icon, IconButton } from "@chakra-ui/react"; 4 | import { SunIcon, MoonIcon } from "@chakra-ui/icons"; 5 | import { useIntl } from "react-intl"; 6 | 7 | export default function ColorModeToggle() { 8 | const { formatMessage } = useIntl(); 9 | const { colorMode, toggleColorMode } = useColorMode(); 10 | const isDark = colorMode === "dark"; 11 | return ( 12 | 17 | ) : ( 18 | 19 | ) 20 | } 21 | aria-label={formatMessage({ 22 | id: "toggle-theme", 23 | description: "Theme toggle label text", 24 | defaultMessage: "Toggle Theme", 25 | })} 26 | onClick={toggleColorMode} 27 | /> 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/badges/ui/zap-circle.tsx: -------------------------------------------------------------------------------- 1 | export default function ZapCircle() { 2 | return ( 3 | 10 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/relays/app/components/color-mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useColorMode, Icon, IconButton } from "@chakra-ui/react"; 4 | import { SunIcon, MoonIcon } from "@chakra-ui/icons"; 5 | import { useIntl } from "react-intl"; 6 | 7 | export default function ColorModeToggle() { 8 | const { formatMessage } = useIntl(); 9 | const { colorMode, toggleColorMode } = useColorMode(); 10 | const isDark = colorMode === "dark"; 11 | return ( 12 | 17 | ) : ( 18 | 19 | ) 20 | } 21 | aria-label={formatMessage({ 22 | id: "toggle-theme", 23 | description: "Theme toggle label text", 24 | defaultMessage: "Toggle Theme", 25 | })} 26 | onClick={toggleColorMode} 27 | /> 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/icons/Close.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./props"; 2 | 3 | export function Close(props: IconProps) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/nostr/emoji.tsx: -------------------------------------------------------------------------------- 1 | import Emoji from "../components/Emoji"; 2 | import { Fragment, Tags } from "../types"; 3 | 4 | export function extractCustomEmoji( 5 | fragments: Fragment[], 6 | tags: Tags, 7 | boxSize = 5, 8 | ) { 9 | return fragments 10 | .map((f) => { 11 | if (typeof f === "string") { 12 | return f.split(/:(\w+):/g).map((i) => { 13 | const t = tags.find((a) => a[0] === "emoji" && a[1] === i); 14 | if (t) { 15 | return ( 16 | 24 | ); 25 | } else { 26 | return i; 27 | } 28 | }); 29 | } 30 | return f; 31 | }) 32 | .flat(); 33 | } 34 | -------------------------------------------------------------------------------- /apps/emojis/app/a/[naddr]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { nip19 } from "nostr-tools"; 5 | import { Alert, AlertIcon } from "@chakra-ui/react"; 6 | 7 | import Layout from "@ui/layout"; 8 | import Address from "@ui/address"; 9 | 10 | export default function AddressPage({ params }: { params: { naddr: string } }) { 11 | const { naddr } = params; 12 | const address = useMemo(() => { 13 | try { 14 | const decoded = nip19.decode(naddr); 15 | if (decoded.type === "naddr") { 16 | return decoded.data; 17 | } 18 | } catch (error) { 19 | console.error(error); 20 | } 21 | }, [naddr]); 22 | 23 | return ( 24 | 25 | {address ? ( 26 |
27 | ) : ( 28 | 29 | 30 | Address not found 31 | 32 | )} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/relays/app/components/relay-list.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Heading, Card, CardHeader, CardBody } from "@chakra-ui/react"; 2 | import { User, EventProps } from "@ngine/core"; 3 | 4 | import RelaySettings from "./relay-settings"; 5 | import { tagToRelay } from "../utils"; 6 | 7 | export default function RelayList({ event }: EventProps) { 8 | return ( 9 | 10 | 11 | 12 | Relays 13 | 14 | 15 | 16 | 17 | 18 | {event.tags 19 | .filter((t) => t[0] === "r") 20 | .map(tagToRelay) 21 | .map((r) => ( 22 | 23 | ))} 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/relays/public/favicon.ico: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /apps/badges/ui/color-mode-switch.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useColorMode, 3 | useColorModeValue, 4 | Switch, 5 | HStack, 6 | Text, 7 | Icon, 8 | } from "@chakra-ui/react"; 9 | import { SunIcon, MoonIcon } from "@chakra-ui/icons"; 10 | 11 | export default function ColorModeSwitch() { 12 | const lightColor = useColorModeValue("orange.500", "orange.200"); 13 | const darkColor = useColorModeValue("gray.300", "gray.500"); 14 | const { colorMode, toggleColorMode } = useColorMode(); 15 | 16 | return ( 17 | 18 | Theme 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/relays/app/components/relay-settings.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, Tag, TagLabel } from "@chakra-ui/react"; 2 | import { Relay } from "@ngine/core"; 3 | 4 | import RelayLink from "./relay-link"; 5 | import ToggleRelay from "./toggle-relay"; 6 | 7 | interface RelaySettingsProps extends Relay { 8 | showToggle?: boolean; 9 | } 10 | 11 | export default function RelaySettings({ 12 | url, 13 | read, 14 | write, 15 | showToggle = true, 16 | }: RelaySettingsProps) { 17 | // todo: edit read/write status 18 | return ( 19 | 20 | 21 | 22 | {read && ( 23 | 24 | read 25 | 26 | )} 27 | {write && ( 28 | 29 | write 30 | 31 | )} 32 | {showToggle && } 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/emojis/app/a/[naddr]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { nip19 } from "nostr-tools"; 5 | import { Alert, AlertIcon } from "@chakra-ui/react"; 6 | import { NDKKind } from "@nostr-dev-kit/ndk"; 7 | 8 | import Layout from "@ui/layout"; 9 | import EditEmojiSet from "@ui/edit-emoji-set"; 10 | 11 | export default function AddressPage({ params }: { params: { naddr: string } }) { 12 | const { naddr } = params; 13 | const address = useMemo(() => { 14 | try { 15 | const decoded = nip19.decode(naddr); 16 | if (decoded.type === "naddr") { 17 | return decoded.data; 18 | } 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | }, [naddr]); 23 | 24 | return ( 25 | 26 | {address && address.kind === NDKKind.EmojiSet ? ( 27 | 28 | ) : ( 29 | 30 | 31 | Can't edit address 32 | 33 | )} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/relays/app/components/relay-metadata.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { Heading, Stack, HStack } from "@chakra-ui/react"; 5 | 6 | import RelayFavicon from "./relay-favicon"; 7 | import RelaySummary from "./relay-summary"; 8 | import ToggleRelay from "./toggle-relay"; 9 | import { RelayMetadata } from "../hooks/useRelayMetadata"; 10 | 11 | export default function Metadata({ 12 | url, 13 | metadata, 14 | }: { 15 | url: string; 16 | metadata: RelayMetadata; 17 | }) { 18 | const domain = useMemo(() => { 19 | return url.replace("ws://", "").replace("wss://", ""); 20 | }, [url]); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | {domain} 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/emojis/app/p/[npub]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { nip19 } from "nostr-tools"; 5 | import { Alert, AlertIcon } from "@chakra-ui/react"; 6 | 7 | import Layout from "@ui/layout"; 8 | import Pubkey from "@ui/pubkey"; 9 | 10 | export default function ProfilePage({ params }: { params: { npub: string } }) { 11 | // todo: not found msg 12 | const { npub } = params; 13 | const pubkey = useMemo(() => { 14 | try { 15 | const decoded = nip19.decode(npub); 16 | if (decoded.type === "npub") { 17 | return decoded.data; 18 | } 19 | if (decoded.type === "nprofile") { 20 | return decoded.data.pubkey; 21 | } 22 | } catch (error) { 23 | console.error(error); 24 | } 25 | }, [npub]); 26 | 27 | return ( 28 | 29 | {pubkey ? ( 30 | 31 | ) : ( 32 | 33 | 34 | Profile not found 35 | 36 | )} 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/relays/app/components/relay-link.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { HStack, Text, Icon } from "@chakra-ui/react"; 3 | import { StarIcon } from "@chakra-ui/icons"; 4 | import { useRelays } from "@ngine/core"; 5 | 6 | import Link from "./link"; 7 | import RelayFavicon from "./relay-favicon"; 8 | import { encodeRelayURL } from "../utils"; 9 | 10 | interface RelayLinkProps { 11 | url: string; 12 | } 13 | 14 | export default function RelayLink({ url }: RelayLinkProps) { 15 | const relays = useRelays(); 16 | const isInMyRelays = useMemo(() => relays.includes(url), [relays, url]); 17 | const encoded = useMemo(() => `/relay/${encodeRelayURL(url)}`, [url]); 18 | const domain = useMemo(() => { 19 | return url.replace("ws://", "").replace("wss://", ""); 20 | }, [url]); 21 | return ( 22 | 23 | 24 | 25 | {domain} 26 | {isInMyRelays && } 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/emojis/ui/edit-emoji-set.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Skeleton } from "@chakra-ui/react"; 4 | import { NDKKind } from "@nostr-dev-kit/ndk"; 5 | import { useEvent, Event } from "@ngine/core"; 6 | 7 | import NewEmojiSet from "./new-emoji-set"; 8 | 9 | interface EventIdProps { 10 | pubkey: string; 11 | identifier: string; 12 | relays?: string[]; 13 | } 14 | 15 | export default function EditEmojiSet({ 16 | pubkey, 17 | identifier, 18 | relays, 19 | }: EventIdProps) { 20 | const event = useEvent( 21 | { 22 | kinds: [NDKKind.EmojiSet], 23 | authors: [pubkey], 24 | "#d": [identifier], 25 | }, 26 | {}, 27 | relays, 28 | ); 29 | const emojis = event?.tags 30 | .filter((t) => t[0] === "emoji") 31 | .map((t) => { 32 | return { 33 | shortcode: t[1], 34 | image: t[2], 35 | }; 36 | }); 37 | 38 | return event ? ( 39 | 44 | ) : ( 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /apps/relays/app/components/relay-icon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "@ngine/core"; 2 | 3 | export default function RelayIcon(props: IconProps) { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alejandro Gómez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/docs/.storybook/main.js: -------------------------------------------------------------------------------- 1 | import { dirname, join, resolve } from "path"; 2 | 3 | function getAbsolutePath(value) { 4 | return dirname(require.resolve(join(value, "package.json"))); 5 | } 6 | 7 | const config = { 8 | stories: ["../stories/*.stories.tsx", "../stories/**/*.stories.tsx"], 9 | addons: [ 10 | getAbsolutePath("@storybook/addon-links"), 11 | getAbsolutePath("@storybook/addon-essentials"), 12 | //getAbsolutePath("@storybook/addon-docs"), 13 | ], 14 | framework: { 15 | name: getAbsolutePath("@storybook/react-vite"), 16 | options: {}, 17 | }, 18 | 19 | core: { 20 | disableTelemetry: true, 21 | }, 22 | 23 | async viteFinal(config, { configType }) { 24 | // customize the Vite config here 25 | return { 26 | ...config, 27 | define: { "process.env": {} }, 28 | resolve: { 29 | alias: [ 30 | { 31 | find: "core", 32 | replacement: resolve(__dirname, "../../../packages/core/"), 33 | }, 34 | ], 35 | }, 36 | }; 37 | }, 38 | 39 | docs: { 40 | autodocs: false, 41 | }, 42 | }; 43 | 44 | export default config; 45 | -------------------------------------------------------------------------------- /apps/relays/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngine/relays", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "eslint . --max-warnings 0" 9 | }, 10 | "dependencies": { 11 | "@chakra-ui/icons": "^2.1.1", 12 | "@chakra-ui/react": "^2.8.2", 13 | "@ngine/core": "workspace:*", 14 | "@nostr-dev-kit/ndk": "^2.3.3", 15 | "@nostr-dev-kit/ndk-cache-dexie": "^2.2.4", 16 | "@tanstack/react-query": "^5.13.4", 17 | "next": "^14.0.3", 18 | "nostr-tools": "^1.17.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-intersection-observer": "^9.5.3", 22 | "react-intl": "^6.5.5" 23 | }, 24 | "devDependencies": { 25 | "@next/eslint-plugin-next": "^14.0.2", 26 | "@repo/eslint-config": "workspace:*", 27 | "@repo/typescript-config": "workspace:*", 28 | "@types/eslint": "^8.44.7", 29 | "@types/node": "^17.0.45", 30 | "@types/react": "^18.0.22", 31 | "@types/react-dom": "^18.0.7", 32 | "eslint": "^8.53.0", 33 | "prettier": "^3.1.1", 34 | "typescript": "^5.2.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/icons/Reply.tsx: -------------------------------------------------------------------------------- 1 | import type { IconProps } from "./props"; 2 | 3 | export function Reply(props: IconProps) { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useFeedback.tsx: -------------------------------------------------------------------------------- 1 | import { useToast, ToastPosition } from "@chakra-ui/react"; 2 | 3 | interface FeedbackParams { 4 | position?: ToastPosition; 5 | duration?: number; 6 | isClosable?: boolean; 7 | } 8 | 9 | interface Feedback { 10 | success: (message: string, title?: string) => void; 11 | error: (message: string, title?: string) => void; 12 | } 13 | 14 | export const defaultOptions = { 15 | position: "top-right" as ToastPosition, 16 | duration: 1500, 17 | isClosable: true, 18 | }; 19 | 20 | export default function useFeedback(opts?: FeedbackParams): Feedback { 21 | const toast = useToast(); 22 | const options = opts ? { ...defaultOptions, ...opts } : defaultOptions; 23 | 24 | return { 25 | success: (message: string, title?: string) => { 26 | toast({ 27 | status: "success", 28 | title, 29 | description: message, 30 | ...options, 31 | }); 32 | }, 33 | error: (message: string, title?: string) => { 34 | toast({ 35 | status: "error", 36 | title, 37 | description: message, 38 | ...options, 39 | }); 40 | }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /apps/docs/stories/zap-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { useDisclosure, Button } from "@chakra-ui/react"; 3 | import { Avatar, ZapButton } from "@ngine/core"; 4 | 5 | const meta: Meta = { 6 | title: "Components / Zap Button", 7 | component: ZapButton, 8 | }; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Primary: Story = { 15 | name: "Pubkey", 16 | args: { 17 | pubkey: "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 18 | }, 19 | }; 20 | 21 | export const Large: Story = { 22 | name: "Large", 23 | args: { 24 | pubkey: "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 25 | size: "lg", 26 | boxSize: 8, 27 | }, 28 | }; 29 | 30 | export const WithAvatar: Story = { 31 | name: "Avatar", 32 | args: { 33 | pubkey: "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 34 | leftIcon: ( 35 | 36 | ), 37 | variant: "outline", 38 | size: "lg", 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /apps/emojis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngine/emojis", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "eslint . --max-warnings 0" 9 | }, 10 | "dependencies": { 11 | "@chakra-ui/icons": "^2.1.1", 12 | "@chakra-ui/react": "^2.8.2", 13 | "@ngine/core": "workspace:*", 14 | "@nostr-dev-kit/ndk": "^2.3.3", 15 | "@nostr-dev-kit/ndk-cache-dexie": "^2.2.4", 16 | "@tanstack/react-query": "^5.13.4", 17 | "@types/lodash.groupby": "^4.6.9", 18 | "lodash.groupby": "^4.6.0", 19 | "next": "^14.0.3", 20 | "nostr-tools": "^2.1.0", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-intl": "^6.5.5" 24 | }, 25 | "devDependencies": { 26 | "@next/eslint-plugin-next": "^14.0.2", 27 | "@repo/eslint-config": "workspace:*", 28 | "@repo/typescript-config": "workspace:*", 29 | "@types/eslint": "^8.44.7", 30 | "@types/node": "^17.0.45", 31 | "@types/react": "^18.0.22", 32 | "@types/react-dom": "^18.0.7", 33 | "eslint": "^8.53.0", 34 | "prettier": "^3.1.1", 35 | "typescript": "^5.2.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/relays/app/relay/[nrelay]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { nip19 } from "nostr-tools"; 5 | import { Alert, AlertIcon } from "@chakra-ui/react"; 6 | import { FormattedMessage } from "react-intl"; 7 | 8 | import Layout from "../../components/layout"; 9 | import Relay from "../../components/relay"; 10 | 11 | export default function RelayPage({ params }: { params: { nrelay: string } }) { 12 | const { nrelay } = params; 13 | const url = useMemo(() => { 14 | try { 15 | const decoded = nip19.decode(nrelay); 16 | if (decoded.type === "nrelay") { 17 | return decoded.data; 18 | } 19 | } catch (error) { 20 | return `wss://${decodeURIComponent(nrelay)}`; 21 | } 22 | }, [nrelay]); 23 | return ( 24 | 25 | {url ? ( 26 | 27 | ) : ( 28 | 29 | 30 | 35 | 36 | )} 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/icons/User.tsx: -------------------------------------------------------------------------------- 1 | import type { IconProps } from "./props"; 2 | 3 | export function User(props: IconProps) { 4 | return ( 5 | 11 | 18 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/eslint-config/storybook.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * typescript packages. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "plugin:storybook/recommended", 17 | "plugin:mdx/recommended", 18 | ...[ 19 | "@vercel/style-guide/eslint/node", 20 | "@vercel/style-guide/eslint/typescript", 21 | "@vercel/style-guide/eslint/browser", 22 | "@vercel/style-guide/eslint/react", 23 | ].map(require.resolve), 24 | ], 25 | parserOptions: { 26 | project, 27 | }, 28 | plugins: ["only-warn"], 29 | globals: { 30 | React: true, 31 | JSX: true, 32 | }, 33 | settings: { 34 | "import/resolver": { 35 | typescript: { 36 | project, 37 | }, 38 | }, 39 | }, 40 | ignorePatterns: ["node_modules/", "dist/"], 41 | // add rules configurations here 42 | rules: { 43 | "import/no-default-export": "off", 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /apps/relays/app/a/[naddr]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { Alert, AlertIcon } from "@chakra-ui/react"; 5 | import { FormattedMessage } from "react-intl"; 6 | import { nip19 } from "nostr-tools"; 7 | 8 | import Address from "../../components/address"; 9 | import Layout from "../../components/layout"; 10 | 11 | export default function AddressPage({ params }: { params: { naddr: string } }) { 12 | const { naddr } = params; 13 | const address = useMemo(() => { 14 | try { 15 | const decoded = nip19.decode(naddr); 16 | if (decoded.type === "naddr") { 17 | return decoded.data; 18 | } 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | }, [naddr]); 23 | 24 | return ( 25 | 26 | {address ? ( 27 |
28 | ) : ( 29 | 30 | 31 | 36 | 37 | )} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/badges/hooks/useBadges.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { NDKKind, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk"; 3 | import { 4 | tagValues, 5 | useLatestEvent, 6 | useEvents, 7 | useAddresses, 8 | } from "@ngine/core"; 9 | 10 | import { getPow } from "@core/rarity"; 11 | 12 | export default function useBadges(pubkey: string) { 13 | const profile = useLatestEvent({ 14 | kinds: [NDKKind.ProfileBadge], 15 | "#d": ["profile_badges"], 16 | authors: [pubkey], 17 | }); 18 | const addresses = profile ? tagValues(profile, "a") : []; 19 | const badgeAdresses = Array.from(new Set(addresses)); 20 | const { events: badgeEvents } = useAddresses(badgeAdresses, { 21 | disable: addresses.length === 0, 22 | cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, 23 | }); 24 | const { events: awards } = useEvents( 25 | { kinds: [NDKKind.BadgeAward], "#p": [pubkey] }, 26 | { 27 | cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, 28 | }, 29 | ); 30 | const badges = useMemo(() => { 31 | const evs = [...badgeEvents]; 32 | return evs.sort((a, b) => getPow(b.id) - getPow(a.id)); 33 | }, [badgeEvents]); 34 | return { profile, badges, awards }; 35 | } 36 | -------------------------------------------------------------------------------- /apps/badges/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngine/badges", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "eslint . --max-warnings 0" 9 | }, 10 | "dependencies": { 11 | "@chakra-ui/icons": "^2.1.1", 12 | "@chakra-ui/react": "^2.8.2", 13 | "@chakra-ui/styled-system": "^2.9.2", 14 | "@fontsource/inter": "^5.0.16", 15 | "@ngine/core": "workspace:*", 16 | "@nostr-dev-kit/ndk": "^2.3.3", 17 | "@nostr-dev-kit/ndk-cache-dexie": "^2.2.4", 18 | "@tanstack/react-query": "^5.13.4", 19 | "next": "^14.0.3", 20 | "nostr-tools": "^2.1.0", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-intl": "^6.5.5", 24 | "slugify": "^1.6.6" 25 | }, 26 | "devDependencies": { 27 | "@next/eslint-plugin-next": "^14.0.2", 28 | "@repo/eslint-config": "workspace:*", 29 | "@repo/typescript-config": "workspace:*", 30 | "@types/eslint": "^8.44.7", 31 | "@types/node": "^17.0.45", 32 | "@types/react": "^18.0.22", 33 | "@types/react-dom": "^18.0.7", 34 | "eslint": "^8.53.0", 35 | "prettier": "^3.1.1", 36 | "typescript": "^5.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/eslint-config/react.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use a library 7 | * that utilizes React. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "@vercel/style-guide/eslint/browser", 17 | "@vercel/style-guide/eslint/typescript", 18 | "@vercel/style-guide/eslint/react", 19 | ].map(require.resolve), 20 | parserOptions: { 21 | project, 22 | }, 23 | plugins: ["only-warn", "formatjs"], 24 | globals: { 25 | JSX: true, 26 | }, 27 | settings: { 28 | "import/resolver": { 29 | typescript: { 30 | project, 31 | }, 32 | }, 33 | }, 34 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js", "**/*.css"], 35 | // add rules configurations here 36 | rules: { 37 | "import/no-default-export": "off", 38 | "formatjs/no-offset": "error", 39 | "formatjs/enforce-description": "literal", 40 | "formatjs/enforce-default-message": "literal" 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /apps/emojis/ui/app.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { 3 | Stack, 4 | Card, 5 | CardBody, 6 | CardFooter, 7 | Heading, 8 | Image, 9 | Link, 10 | } from "@chakra-ui/react"; 11 | import { NDKKind } from "@nostr-dev-kit/ndk"; 12 | import { useProfile, EventProps, Reactions } from "@ngine/core"; 13 | 14 | export default function App({ event }: EventProps) { 15 | const author = useProfile(event.pubkey); 16 | const app = useMemo(() => { 17 | try { 18 | return JSON.parse(event.content); 19 | } catch (e) { 20 | return author; 21 | } 22 | }, [event]); 23 | const href = app?.website; 24 | return ( 25 | 26 | 27 | 28 | 29 | {app?.display_name || app?.name} 30 | {href && ( 31 | 32 | {href} 33 | 34 | )} 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useLatestEvent.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from "react"; 2 | 3 | import { 4 | NDKEvent, 5 | NDKFilter, 6 | NDKSubscriptionOptions, 7 | NDKRelaySet, 8 | } from "@nostr-dev-kit/ndk"; 9 | 10 | import { useNDK } from "../context"; 11 | import { SubscriptionOptions } from "./useEvents"; 12 | 13 | export default function useLatestEvent( 14 | filter: NDKFilter | NDKFilter[], 15 | opts?: SubscriptionOptions, 16 | relays?: string[], 17 | ) { 18 | const ndk = useNDK(); 19 | const [event, setEvent] = useState(); 20 | 21 | useEffect(() => { 22 | if (!opts?.disable) { 23 | const relaySet = 24 | relays?.length ?? 0 > 0 25 | ? NDKRelaySet.fromRelayUrls(relays as string[], ndk) 26 | : undefined; 27 | const sub = ndk.subscribe(filter, opts, relaySet); 28 | sub.on("event", (ev: NDKEvent) => { 29 | const lastSeen = event?.created_at ?? 0; 30 | const createdAt = ev?.created_at ?? 0; 31 | if (createdAt > lastSeen) { 32 | setEvent(ev); 33 | } 34 | }); 35 | return () => { 36 | sub.stop(); 37 | }; 38 | } 39 | }, [opts?.disable]); 40 | 41 | return event; 42 | } 43 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/docs", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "storybook dev -p 6006", 8 | "build": "storybook build", 9 | "preview-storybook": "serve storybook-static", 10 | "clean": "rm -rf .turbo && rm -rf node_modules", 11 | "lint": "eslint ./stories/*.stories.tsx --max-warnings 0" 12 | }, 13 | "dependencies": { 14 | "@chakra-ui/react": "^2.8.2", 15 | "@ngine/core": "workspace:*", 16 | "@nostr-dev-kit/ndk": "2.3.0", 17 | "@nostr-dev-kit/ndk-cache-dexie": "^2.2.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0" 20 | }, 21 | "devDependencies": { 22 | "@repo/eslint-config": "workspace:*", 23 | "@repo/typescript-config": "workspace:*", 24 | "@storybook/addon-actions": "^7.5.3", 25 | "@storybook/addon-docs": "^7.5.3", 26 | "@storybook/addon-essentials": "^7.5.3", 27 | "@storybook/addon-links": "^7.5.3", 28 | "@storybook/react": "^7.5.3", 29 | "@storybook/react-vite": "^7.5.3", 30 | "@vitejs/plugin-react": "^4.2.0", 31 | "eslint": "^8.54.0", 32 | "serve": "^14.2.1", 33 | "storybook": "^7.5.3", 34 | "typescript": "^5.3.2", 35 | "vite": "^5.0.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/docs/stories/login.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Stack } from "@chakra-ui/react"; 3 | import { Login, LoginMenu } from "@ngine/core"; 4 | 5 | const meta: Meta = { 6 | title: "Components / Login", 7 | component: Login, 8 | }; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Extension: Story = { 15 | name: "Extension", 16 | render(props) { 17 | return ( 18 | 19 | 20 | 21 | 22 | ); 23 | }, 24 | args: { 25 | method: "nip07", 26 | }, 27 | }; 28 | 29 | export const Pubkey: Story = { 30 | name: "Pubkey", 31 | render(props) { 32 | return ( 33 | 34 | 35 | 36 | 37 | ); 38 | }, 39 | args: { 40 | method: "npub", 41 | }, 42 | }; 43 | 44 | export const Bunker: Story = { 45 | name: "Bunker", 46 | render(props) { 47 | return ( 48 | 49 | 50 | 51 | 52 | ); 53 | }, 54 | args: { 55 | method: "nip46", 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/core/src/icons/Dots.tsx: -------------------------------------------------------------------------------- 1 | import type { IconProps } from "./props"; 2 | 3 | export function Dots(props: IconProps) { 4 | return ( 5 | 13 | 19 | 25 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup Node.js 18.x 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 18 22 | 23 | - name: Install Dependencies 24 | run: yarn 25 | 26 | - name: Create Release Pull Request or Publish to npm 27 | id: changesets 28 | uses: changesets/action@v1 29 | with: 30 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 31 | publish: yarn release 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | 36 | - name: Send a Slack notification if a publish happens 37 | if: steps.changesets.outputs.published == 'true' 38 | # You can do something when a publish happens. 39 | run: my-slack-bot send-notification --message "A new version of ${GITHUB_REPOSITORY} was published!" 40 | -------------------------------------------------------------------------------- /apps/emojis/app/e/[nevent]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { Alert, AlertIcon } from "@chakra-ui/react"; 5 | import { FormattedMessage } from "react-intl"; 6 | import { nip19 } from "nostr-tools"; 7 | 8 | import Layout from "@ui/layout"; 9 | import Event from "@ui/event"; 10 | 11 | export default function EventPage({ params }: { params: { nevent: string } }) { 12 | const { nevent } = params; 13 | const event = useMemo(() => { 14 | try { 15 | const decoded = nip19.decode(nevent); 16 | if (decoded.type === "nevent") { 17 | return decoded.data; 18 | } 19 | if (decoded.type === "note") { 20 | return { id: decoded.data, relays: [] }; 21 | } 22 | } catch (error) { 23 | console.error(error); 24 | } 25 | }, [nevent]); 26 | 27 | return ( 28 | 29 | {event ? ( 30 | 31 | ) : ( 32 | 33 | 34 | 39 | 40 | )} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useEvent.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from "react"; 2 | 3 | import { 4 | NDKEvent, 5 | NDKFilter, 6 | NDKRelaySet, 7 | NDKSubscriptionCacheUsage, 8 | } from "@nostr-dev-kit/ndk"; 9 | import { useQuery, UseQueryResult } from "@tanstack/react-query"; 10 | 11 | import { useNDK } from "../context"; 12 | import { hashSha256 } from "../utils"; 13 | import { SubscriptionOptions } from "./useEvents"; 14 | 15 | export default function useEvent( 16 | filter: NDKFilter, 17 | opts?: SubscriptionOptions, 18 | relays?: string[], 19 | ) { 20 | const ndk = useNDK(); 21 | const id = useMemo(() => { 22 | return hashSha256(filter); 23 | }, [filter]); 24 | 25 | const query: UseQueryResult = useQuery({ 26 | queryKey: ["use-event", id], 27 | queryFn: () => { 28 | const relaySet = 29 | relays?.length ?? 0 > 0 30 | ? NDKRelaySet.fromRelayUrls(relays as string[], ndk) 31 | : undefined; 32 | return ndk.fetchEvent( 33 | filter, 34 | { 35 | groupable: true, 36 | cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, 37 | ...(opts ? opts : {}), 38 | }, 39 | relaySet, 40 | ); 41 | }, 42 | }); 43 | 44 | return query.data; 45 | } 46 | -------------------------------------------------------------------------------- /apps/relays/app/e/[nevent]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { Alert, AlertIcon } from "@chakra-ui/react"; 5 | import { FormattedMessage } from "react-intl"; 6 | import { nip19 } from "nostr-tools"; 7 | 8 | import Event from "../../components/event"; 9 | import Layout from "../../components/layout"; 10 | 11 | export default function EventPage({ params }: { params: { nevent: string } }) { 12 | const { nevent } = params; 13 | const event = useMemo(() => { 14 | try { 15 | const decoded = nip19.decode(nevent); 16 | if (decoded.type === "nevent") { 17 | return decoded.data; 18 | } 19 | if (decoded.type === "note") { 20 | return { id: decoded.data, relays: [] }; 21 | } 22 | } catch (error) { 23 | console.error(error); 24 | } 25 | }, [nevent]); 26 | 27 | return ( 28 | 29 | {event ? ( 30 | 31 | ) : ( 32 | 33 | 34 | 39 | 40 | )} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/components/ZapButton.tsx: -------------------------------------------------------------------------------- 1 | import { useDisclosure, Icon, Button } from "@chakra-ui/react"; 2 | import type { ButtonProps, IconProps } from "@chakra-ui/react"; 3 | import type { NDKEvent } from "@nostr-dev-kit/ndk"; 4 | import { FormattedMessage } from "react-intl"; 5 | 6 | import ZapModal from "./ZapModal"; 7 | import { ZapCircle } from "../icons"; 8 | 9 | interface ZapButtonProps extends ButtonProps, Pick { 10 | pubkey: string; 11 | event?: NDKEvent; 12 | } 13 | 14 | export default function ZapButton({ 15 | pubkey, 16 | event, 17 | boxSize = 4, 18 | ...rest 19 | }: ZapButtonProps) { 20 | const { isOpen, onOpen, onClose } = useDisclosure(); 21 | return ( 22 | <> 23 | 37 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/components/ReactionIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Text, Icon, TextProps, ImageProps, HStack } from "@chakra-ui/react"; 2 | import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; 3 | 4 | import Emoji from "./Emoji"; 5 | import { Heart, Repost, Bookmark } from "../icons"; 6 | import { REPOSTS, BOOKMARKS } from "../nostr/kinds"; 7 | import { extractCustomEmoji } from "../nostr/emoji"; 8 | 9 | interface ReactionIconProps extends Pick { 10 | boxSize?: number; 11 | event: NDKEvent; 12 | } 13 | 14 | export default function ReactionIcon({ 15 | event, 16 | boxSize = 4, 17 | fontSize = "md", 18 | }: ReactionIconProps) { 19 | if (REPOSTS.includes(event.kind as number)) { 20 | return ; 21 | } 22 | 23 | if (BOOKMARKS.includes(event.kind as number)) { 24 | return ; 25 | } 26 | 27 | const hasCustomEmoji = event.tags.find((t) => t[0] === "emoji"); 28 | return hasCustomEmoji ? ( 29 | 30 | {extractCustomEmoji([event.content], event.tags, boxSize)} 31 | 32 | ) : !["+", "-"].includes(event.content) ? ( 33 | {event.content} 34 | ) : ( 35 | // todo: dislike 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/relays/app/hooks/useRelayMetadata.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, useQueries, UseQueryResult } from "@tanstack/react-query"; 2 | 3 | export type RelayMetadata = Record; 4 | 5 | async function getRelayMetadata(url: string) { 6 | try { 7 | const relayUrl = new URL(url); 8 | const isSecure = url.startsWith("wss://"); 9 | const relayInfoUrl = `${isSecure ? "https" : "http"}://${relayUrl.host}${ 10 | relayUrl.pathname 11 | }`; 12 | return await fetch(relayInfoUrl, { 13 | headers: { 14 | Accept: "application/nostr+json", 15 | }, 16 | }).then((res) => res.json()); 17 | } catch (error) { 18 | console.error(`Couldn't fetch NIP-11 metadata for ${url}`); 19 | } 20 | } 21 | 22 | export function useRelayMetadata( 23 | url: string, 24 | ): UseQueryResult { 25 | return useQuery({ 26 | queryKey: ["relay-metadata", url], 27 | queryFn: () => getRelayMetadata(url), 28 | retry: false, 29 | retryOnMount: false, 30 | }); 31 | } 32 | 33 | export function useRelaysMetadata(urls: string[]) { 34 | return useQueries({ 35 | queries: urls.map((url) => { 36 | return { 37 | queryKey: ["relay-metadata", url], 38 | queryFn: () => getRelayMetadata(url), 39 | retry: false, 40 | retryOnMount: false, 41 | }; 42 | }), 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/components/UnknownKind.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, Card, CardHeader, CardBody, Text } from "@chakra-ui/react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | import User from "./User"; 5 | import Blockquote from "./Blockquote"; 6 | import Markdown from "./Markdown"; 7 | import { EventProps } from "../types"; 8 | 9 | import RecommendedAppMenu from "./RecommendedAppMenu"; 10 | 11 | export default function UnknownKind({ event }: EventProps) { 12 | const alt = event.tagValue("alt"); 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {alt ? ( 24 |
25 | 26 |
27 | ) : ( 28 | 29 | 35 | 36 | )} 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/emojis/ui/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, ReactElement } from "react"; 4 | 5 | import NDK from "@nostr-dev-kit/ndk"; 6 | import NDKCacheAdapterDexie from "@nostr-dev-kit/ndk-cache-dexie"; 7 | import { NgineProvider } from "@ngine/core"; 8 | 9 | import Link from "./link"; 10 | import Container from "./container"; 11 | import Main from "./main"; 12 | import Header from "./header"; 13 | import Footer from "./footer"; 14 | import { theme } from "./theme"; 15 | 16 | const cacheAdapter = new NDKCacheAdapterDexie({ dbName: "emojis" }); 17 | const ndk = new NDK({ 18 | explicitRelayUrls: [ 19 | "wss://nos.lol", 20 | "wss://relay.nostr.band", 21 | "wss://frens.nostr1.com", 22 | ], 23 | outboxRelayUrls: ["wss://purplepag.es"], 24 | enableOutboxModel: true, 25 | cacheAdapter, 26 | }); 27 | 28 | export default function Layout({ children }: { children: any }) { 29 | useEffect(() => { 30 | ndk.connect(); 31 | }, []); 32 | 33 | return ( 34 | `/p/${npub}`, 40 | nprofile: (nprofile) => `/p/${nprofile}`, 41 | }} 42 | > 43 | 44 |
45 |
{children}
46 |