diff --git a/package-lock.json b/package-lock.json index f070591d..e972c742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jumble-imwald", - "version": "18.0.3", + "version": "19.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jumble-imwald", - "version": "18.0.3", + "version": "19.0.0", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", @@ -33,15 +33,18 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toast": "^1.2.4", "@tailwindcss/typography": "^0.5.16", + "@tiptap/core": "^2.12.0", + "@tiptap/extension-document": "^2.12.0", "@tiptap/extension-emoji": "^2.26.1", + "@tiptap/extension-hard-break": "^2.12.0", "@tiptap/extension-history": "^2.12.0", "@tiptap/extension-mention": "^2.12.0", + "@tiptap/extension-paragraph": "^2.12.0", "@tiptap/extension-placeholder": "^2.12.0", + "@tiptap/extension-text": "^2.12.0", "@tiptap/pm": "^2.12.0", "@tiptap/react": "^2.12.0", - "@tiptap/starter-kit": "^2.12.0", "@tiptap/suggestion": "^2.12.0", "@webbtc/webln-types": "^3.0.0", "blossom-client-sdk": "^4.1.0", @@ -55,17 +58,16 @@ "embla-carousel-wheel-gestures": "^8.1.0", "emoji-picker-react": "^4.12.2", "flexsearch": "^0.7.43", - "franc-min": "^6.2.0", "highlight.js": "^11.9.0", "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.0.4", "katex": "^0.16.25", "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", - "next-themes": "^0.4.6", "nostr-tools": "^2.17.0", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.2.0", + "prosemirror-state": "^1.4.3", "qr-code-styling": "^1.9.2", "qr-scanner": "^1.4.2", "react": "^18.3.1", @@ -84,7 +86,6 @@ "@eslint/js": "^9.17.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", - "@testing-library/user-event": "^14.6.1", "@types/node": "^22.10.2", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", @@ -4175,40 +4176,6 @@ } } }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -5036,20 +5003,6 @@ } } }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, "node_modules/@tiptap/core": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.2.tgz", @@ -5063,32 +5016,6 @@ "@tiptap/pm": "^2.7.0" } }, - "node_modules/@tiptap/extension-blockquote": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.27.2.tgz", - "integrity": "sha512-oIGZgiAeA4tG3YxbTDfrmENL4/CIwGuP3THtHsNhwRqwsl9SfMk58Ucopi2GXTQSdYXpRJ0ahE6nPqB5D6j/Zw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, - "node_modules/@tiptap/extension-bold": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.27.2.tgz", - "integrity": "sha512-bR7J5IwjCGQ0s3CIxyMvOCnMFMzIvsc5OVZKscTN5UkXzFsaY6muUAIqtKxayBUucjtUskm5qZowJITCeCb1/A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, "node_modules/@tiptap/extension-bubble-menu": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.27.2.tgz", @@ -5106,46 +5033,6 @@ "@tiptap/pm": "^2.7.0" } }, - "node_modules/@tiptap/extension-bullet-list": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.27.2.tgz", - "integrity": "sha512-gmFuKi97u5f8uFc/GQs+zmezjiulZmFiDYTh3trVoLRoc2SAHOjGEB7qxdx7dsqmMN7gwiAWAEVurLKIi1lnnw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, - "node_modules/@tiptap/extension-code": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.27.2.tgz", - "integrity": "sha512-7X9AgwqiIGXoZX7uvdHQsGsjILnN/JaEVtqfXZnPECzKGaWHeK/Ao4sYvIIIffsyZJA8k5DC7ny2/0sAgr2TuA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, - "node_modules/@tiptap/extension-code-block": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.27.2.tgz", - "integrity": "sha512-KgvdQHS4jXr79aU3wZOGBIZYYl9vCB7uDEuRFV4so2rYrfmiYMw3T8bTnlNEEGe4RUeAms1i4fdwwvQp9nR1Dw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0", - "@tiptap/pm": "^2.7.0" - } - }, "node_modules/@tiptap/extension-document": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.27.2.tgz", @@ -5159,20 +5046,6 @@ "@tiptap/core": "^2.7.0" } }, - "node_modules/@tiptap/extension-dropcursor": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.27.2.tgz", - "integrity": "sha512-oEu/OrktNoQXq1x29NnH/GOIzQZm8ieTQl3FK27nxfBPA89cNoH4mFEUmBL5/OFIENIjiYG3qWpg6voIqzswNw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0", - "@tiptap/pm": "^2.7.0" - } - }, "node_modules/@tiptap/extension-emoji": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/extension-emoji/-/extension-emoji-2.27.2.tgz", @@ -5210,20 +5083,6 @@ "@tiptap/pm": "^2.7.0" } }, - "node_modules/@tiptap/extension-gapcursor": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.27.2.tgz", - "integrity": "sha512-/c9VF1HBxj+AP54XGVgCmD9bEGYc5w5OofYCFQgM7l7PB1J00A4vOke0oPkHJnqnOOyPlFaxO/7N6l3XwFcnKA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0", - "@tiptap/pm": "^2.7.0" - } - }, "node_modules/@tiptap/extension-hard-break": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.27.2.tgz", @@ -5237,19 +5096,6 @@ "@tiptap/core": "^2.7.0" } }, - "node_modules/@tiptap/extension-heading": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.27.2.tgz", - "integrity": "sha512-iM3yeRWuuQR/IRQ1djwNooJGfn9Jts9zF43qZIUf+U2NY8IlvdNsk2wTOdBgh6E0CamrStPxYGuln3ZS4fuglw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, "node_modules/@tiptap/extension-history": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.27.2.tgz", @@ -5264,46 +5110,6 @@ "@tiptap/pm": "^2.7.0" } }, - "node_modules/@tiptap/extension-horizontal-rule": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.27.2.tgz", - "integrity": "sha512-WGWUSgX+jCsbtf9Y9OCUUgRZYuwjVoieW5n6mAUohJ9/6gc6sGIOrUpBShf+HHo6WD+gtQjRd+PssmX3NPWMpg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0", - "@tiptap/pm": "^2.7.0" - } - }, - "node_modules/@tiptap/extension-italic": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.27.2.tgz", - "integrity": "sha512-1OFsw2SZqfaqx5Fa5v90iNlPRcqyt+lVSjBwTDzuPxTPFY4Q0mL89mKgkq2gVHYNCiaRkXvFLDxaSvBWbmthgg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, - "node_modules/@tiptap/extension-list-item": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.27.2.tgz", - "integrity": "sha512-eJNee7IEGXMnmygM5SdMGDC8m/lMWmwNGf9fPCK6xk0NxuQRgmZHL6uApKcdH6gyNcRPHCqvTTkhEP7pbny/fg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, "node_modules/@tiptap/extension-mention": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.27.2.tgz", @@ -5319,19 +5125,6 @@ "@tiptap/suggestion": "^2.7.0" } }, - "node_modules/@tiptap/extension-ordered-list": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.27.2.tgz", - "integrity": "sha512-M7A4tLGJcLPYdLC4CI2Gwl8LOrENQW59u3cMVa+KkwG1hzSJyPsbDpa1DI6oXPC2WtYiTf22zrbq3gVvH+KA2w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, "node_modules/@tiptap/extension-paragraph": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.27.2.tgz", @@ -5359,19 +5152,6 @@ "@tiptap/pm": "^2.7.0" } }, - "node_modules/@tiptap/extension-strike": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.27.2.tgz", - "integrity": "sha512-HHIjhafLhS2lHgfAsCwC1okqMsQzR4/mkGDm4M583Yftyjri1TNA7lzhzXWRFWiiMfJxKtdjHjUAQaHuteRTZw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, "node_modules/@tiptap/extension-text": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.27.2.tgz", @@ -5385,19 +5165,6 @@ "@tiptap/core": "^2.7.0" } }, - "node_modules/@tiptap/extension-text-style": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.27.2.tgz", - "integrity": "sha512-Omk+uxjJLyEY69KStpCw5fA9asvV+MGcAX2HOxyISDFoLaL49TMrNjhGAuz09P1L1b0KGXo4ml7Q3v/Lfy4WPA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, "node_modules/@tiptap/pm": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.2.tgz", @@ -5451,39 +5218,6 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@tiptap/starter-kit": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.27.2.tgz", - "integrity": "sha512-bb0gJvPoDuyRUQ/iuN52j1//EtWWttw+RXAv1uJxfR0uKf8X7uAqzaOOgwjknoCIDC97+1YHwpGdnRjpDkOBxw==", - "license": "MIT", - "dependencies": { - "@tiptap/core": "^2.27.2", - "@tiptap/extension-blockquote": "^2.27.2", - "@tiptap/extension-bold": "^2.27.2", - "@tiptap/extension-bullet-list": "^2.27.2", - "@tiptap/extension-code": "^2.27.2", - "@tiptap/extension-code-block": "^2.27.2", - "@tiptap/extension-document": "^2.27.2", - "@tiptap/extension-dropcursor": "^2.27.2", - "@tiptap/extension-gapcursor": "^2.27.2", - "@tiptap/extension-hard-break": "^2.27.2", - "@tiptap/extension-heading": "^2.27.2", - "@tiptap/extension-history": "^2.27.2", - "@tiptap/extension-horizontal-rule": "^2.27.2", - "@tiptap/extension-italic": "^2.27.2", - "@tiptap/extension-list-item": "^2.27.2", - "@tiptap/extension-ordered-list": "^2.27.2", - "@tiptap/extension-paragraph": "^2.27.2", - "@tiptap/extension-strike": "^2.27.2", - "@tiptap/extension-text": "^2.27.2", - "@tiptap/extension-text-style": "^2.27.2", - "@tiptap/pm": "^2.27.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - } - }, "node_modules/@tiptap/suggestion": { "version": "2.27.2", "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.27.2.tgz", @@ -6688,16 +6422,6 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/collapse-white-space": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", - "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -7748,9 +7472,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -7807,19 +7531,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/franc-min": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/franc-min/-/franc-min-6.2.0.tgz", - "integrity": "sha512-1uDIEUSlUZgvJa2AKYR/dmJC66v/PvGQ9mWfI9nOr/kPpMFyvswK0gPXOwpYJYiYD008PpHLkGfG58SPjQJFxw==", - "license": "MIT", - "dependencies": { - "trigram-utils": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -9294,16 +9005,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/n-gram": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/n-gram/-/n-gram-2.0.2.tgz", - "integrity": "sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9329,16 +9030,6 @@ "dev": true, "license": "MIT" }, - "node_modules/next-themes": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" - } - }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", @@ -11506,20 +11197,6 @@ "node": ">=20" } }, - "node_modules/trigram-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/trigram-utils/-/trigram-utils-2.0.1.tgz", - "integrity": "sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==", - "license": "MIT", - "dependencies": { - "collapse-white-space": "^2.0.0", - "n-gram": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", diff --git a/package.json b/package.json index 80129a40..61c944a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jumble-imwald", - "version": "18.0.3", + "version": "19.0.0", "description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble", "private": true, "type": "module", @@ -45,16 +45,20 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toast": "^1.2.4", "@tailwindcss/typography": "^0.5.16", + "@tiptap/core": "^2.12.0", + "@tiptap/extension-document": "^2.12.0", "@tiptap/extension-emoji": "^2.26.1", + "@tiptap/extension-hard-break": "^2.12.0", "@tiptap/extension-history": "^2.12.0", "@tiptap/extension-mention": "^2.12.0", + "@tiptap/extension-paragraph": "^2.12.0", "@tiptap/extension-placeholder": "^2.12.0", + "@tiptap/extension-text": "^2.12.0", "@tiptap/pm": "^2.12.0", "@tiptap/react": "^2.12.0", - "@tiptap/starter-kit": "^2.12.0", "@tiptap/suggestion": "^2.12.0", + "prosemirror-state": "^1.4.3", "@webbtc/webln-types": "^3.0.0", "blossom-client-sdk": "^4.1.0", "blurhash": "^2.0.5", @@ -67,14 +71,12 @@ "embla-carousel-wheel-gestures": "^8.1.0", "emoji-picker-react": "^4.12.2", "flexsearch": "^0.7.43", - "franc-min": "^6.2.0", "highlight.js": "^11.9.0", "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.0.4", "katex": "^0.16.25", "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", - "next-themes": "^0.4.6", "nostr-tools": "^2.17.0", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.2.0", @@ -96,7 +98,6 @@ "@eslint/js": "^9.17.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", - "@testing-library/user-event": "^14.6.1", "@types/node": "^22.10.2", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", diff --git a/src/components/Note/AsciidocArticle/AsciidocArticle.tsx b/src/components/Note/AsciidocArticle/AsciidocArticle.tsx index 9c6e8408..7a127c73 100644 --- a/src/components/Note/AsciidocArticle/AsciidocArticle.tsx +++ b/src/components/Note/AsciidocArticle/AsciidocArticle.tsx @@ -31,7 +31,7 @@ import logger from '@/lib/logger' import { extractBookMetadata } from '@/lib/bookstr-parser' import { ExtendedKind } from '@/constants' import katex from 'katex' -import 'katex/dist/katex.min.css' +import '@/styles/katex-bundle.css' import { WS_URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants' /** diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index 8d92efe6..486650d2 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -29,7 +29,7 @@ import { preprocessMarkdownMediaLinks } from './preprocessMarkup' import { PAYTO_URI_REGEX, parsePaytoUri } from '@/lib/payto' import PaytoLink from '@/components/PaytoLink' import katex from 'katex' -import 'katex/dist/katex.min.css' +import '@/styles/katex-bundle.css' import { isContentSpacingDebug, reprString } from '@/lib/content-spacing-debug' import logger from '@/lib/logger' diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index e0739b62..45d4025b 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,11 +1,9 @@ -// import { useTheme } from "next-themes" import { useTheme } from '@/providers/ThemeProvider' import { Toaster as Sonner } from 'sonner' type ToasterProps = React.ComponentProps const Toaster = ({ ...props }: ToasterProps) => { - // const { theme = "system" } = useTheme() const { themeSetting } = useTheme() return ( diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index f6a1914d..57c33890 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -430,22 +430,6 @@ export default { 'Are you absolutely sure?': 'هل أنت متأكد تماماً؟', 'You will not be able to send zaps to others.': 'لن تتمكن من إرسال zaps للآخرين.', Disconnect: 'قطع الاتصال', - 'Start with a Rizful Vault': 'ابدأ بمحفظة Rizful', - 'or other wallets': 'أو محافظ أخرى', - 'Rizful Vault': 'محفظة Rizful', - 'Rizful Vault connected!': 'تم توصيل محفظة Rizful!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'يمكنك الآن استخدام محفظة Rizful الخاصة بك لإرسال zap إلى ملاحظاتك والمبدعين المفضلين لديك.', - 'Your Lightning Address': 'عنوان Lightning الخاص بك', - 'New to Rizful?': 'جديد في Rizful؟', - 'Sign up for Rizful': 'سجل في Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'إذا كان لديك حساب Rizful بالفعل، يمكنك تخطي هذه الخطوة.', - 'Get your one-time code': 'احصل على رمز الاستخدام مرة واحدة', - 'Get code': 'احصل على الرمز', - 'Connect to your Rizful Vault': 'الاتصال بمحفظة Rizful الخاصة بك', - 'Paste your one-time code here': 'الصق رمز الاستخدام مرة واحدة هنا', - Connect: 'اتصال', 'Set up your wallet to send and receive sats!': 'قم بإعداد محفظتك لإرسال واستقبال الساتس!', 'Set up': 'إعداد' } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 709aef32..08d7d275 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -637,22 +637,6 @@ export default { 'You will not be able to send zaps to others.': 'Du wirst keine Zaps mehr an andere senden können.', Disconnect: 'Trennen', - 'Start with a Rizful Vault': 'Starte mit einem Rizful Vault', - 'or other wallets': 'oder andere Wallets', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vault verbunden!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'Du kannst jetzt dein Rizful Vault verwenden, um deine Lieblingsnotizen und -ersteller zu zapen.', - 'Your Lightning Address': 'Deine Lightning-Adresse', - 'New to Rizful?': 'Neu bei Rizful?', - 'Sign up for Rizful': 'Registriere dich bei Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'Wenn du bereits ein Rizful-Konto hast, kannst du diesen Schritt überspringen.', - 'Get your one-time code': 'Hole dir deinen Einmal-Code', - 'Get code': 'Code holen', - 'Connect to your Rizful Vault': 'Verbinde dich mit deinem Rizful Vault', - 'Paste your one-time code here': 'Füge hier deinen Einmal-Code ein', - Connect: 'Verbinden', 'Set up your wallet to send and receive sats!': 'Richte deine Wallet ein, um Sats zu senden und zu empfangen!', 'Set up': 'Einrichten', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 48823d29..136cfd69 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -704,22 +704,6 @@ export default { 'Are you absolutely sure?': 'Are you absolutely sure?', 'You will not be able to send zaps to others.': 'You will not be able to send zaps to others.', Disconnect: 'Disconnect', - 'Start with a Rizful Vault': 'Start with a Rizful Vault', - 'or other wallets': 'or other wallets', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vault connected!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'You can now use your Rizful Vault to zap your favorite notes and creators.', - 'Your Lightning Address': 'Your Lightning Address', - 'New to Rizful?': 'New to Rizful?', - 'Sign up for Rizful': 'Sign up for Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'If you already have a Rizful account, you can skip this step.', - 'Get your one-time code': 'Get your one-time code', - 'Get code': 'Get code', - 'Connect to your Rizful Vault': 'Connect to your Rizful Vault', - 'Paste your one-time code here': 'Paste your one-time code here', - Connect: 'Connect', 'Set up your wallet to send and receive sats!': 'Set up your wallet to send and receive sats!', 'Set up': 'Set up', 'nested events': 'nested events', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index d817ef73..23520e2d 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -437,22 +437,6 @@ export default { 'Are you absolutely sure?': '¿Estás absolutamente seguro?', 'You will not be able to send zaps to others.': 'No podrás enviar zaps a otros.', Disconnect: 'Desconectar', - 'Start with a Rizful Vault': 'Comienza con una Bóveda Rizful', - 'or other wallets': 'o otras billeteras', - 'Rizful Vault': 'Bóveda Rizful', - 'Rizful Vault connected!': '¡Bóveda Rizful conectada!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'Ahora puedes usar tu Bóveda Rizful para zapear tus notas y creadores favoritos.', - 'Your Lightning Address': 'Tu Dirección Lightning', - 'New to Rizful?': '¿Nuevo en Rizful?', - 'Sign up for Rizful': 'Regístrate en Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'Si ya tienes una cuenta de Rizful, puedes omitir este paso.', - 'Get your one-time code': 'Obtén tu código de un solo uso', - 'Get code': 'Obtener código', - 'Connect to your Rizful Vault': 'Conéctate a tu Bóveda Rizful', - 'Paste your one-time code here': 'Pega tu código de un solo uso aquí', - Connect: 'Conectar', 'Set up your wallet to send and receive sats!': '¡Configura tu billetera para enviar y recibir sats!', 'Set up': 'Configurar' diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index b06bf24a..0d05b9cf 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -432,22 +432,6 @@ export default { 'Are you absolutely sure?': 'آیا کاملاً مطمئن هستید؟', 'You will not be able to send zaps to others.': 'شما قادر نخواهید بود به دیگران زپ ارسال کنید.', Disconnect: 'قطع اتصال', - 'Start with a Rizful Vault': 'شروع با Rizful Vault', - 'or other wallets': 'یا کیف پول‌های دیگر', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vault متصل شد!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'اکنون می‌توانید از Rizful Vault خود برای زپ کردن یادداشت‌ها و سازندگان مورد علاقه خود استفاده کنید.', - 'Your Lightning Address': 'آدرس لایتنینگ شما', - 'New to Rizful?': 'جدید در Rizful؟', - 'Sign up for Rizful': 'برای Rizful ثبت نام کنید', - 'If you already have a Rizful account, you can skip this step.': - 'اگر قبلاً حساب Rizful دارید، می‌توانید این مرحله را رد کنید.', - 'Get your one-time code': 'کد یکبار مصرف خود را دریافت کنید', - 'Get code': 'دریافت کد', - 'Connect to your Rizful Vault': 'اتصال به Rizful Vault خود', - 'Paste your one-time code here': 'کد یکبار مصرف خود را اینجا بچسبانید', - Connect: 'اتصال', 'Set up your wallet to send and receive sats!': 'کیف پولت را تنظیم کن تا ساتس ارسال و دریافت کنی!', 'Set up': 'تنظیم' diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 5fcca787..b3a2789e 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -442,22 +442,6 @@ export default { 'You will not be able to send zaps to others.': 'Vous ne pourrez plus envoyer de zaps aux autres.', Disconnect: 'Déconnecter', - 'Start with a Rizful Vault': 'Démarrer avec un coffre Rizful', - 'or other wallets': 'ou d’autres portefeuilles', - 'Rizful Vault': 'Coffre Rizful', - 'Rizful Vault connected!': 'Coffre Rizful connecté !', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'Vous pouvez maintenant utiliser votre coffre Rizful pour zapper vos notes et créateurs préférés.', - 'Your Lightning Address': 'Votre adresse Lightning', - 'New to Rizful?': 'Nouveau sur Rizful ?', - 'Sign up for Rizful': 'Inscrivez-vous sur Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'Si vous avez déjà un compte Rizful, vous pouvez passer cette étape.', - 'Get your one-time code': 'Obtenez votre code à usage unique', - 'Get code': 'Obtenir le code', - 'Connect to your Rizful Vault': 'Connectez-vous à votre coffre Rizful', - 'Paste your one-time code here': 'Collez votre code à usage unique ici', - Connect: 'Connecter', 'Set up your wallet to send and receive sats!': 'Configurez votre portefeuille pour envoyer et recevoir des sats !', 'Set up': 'Configurer' diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index ca11af0e..3f0e1b80 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -434,22 +434,6 @@ export default { 'Are you absolutely sure?': 'क्या आप पूरी तरह से सुनिश्चित हैं?', 'You will not be able to send zaps to others.': 'आप दूसरों को जैप नहीं भेज पाएंगे।', Disconnect: 'डिस्कनेक्ट करें', - 'Start with a Rizful Vault': 'Rizful वॉल्ट के साथ शुरू करें', - 'or other wallets': 'या अन्य वॉलेट', - 'Rizful Vault': 'Rizful वॉल्ट', - 'Rizful Vault connected!': 'Rizful वॉल्ट कनेक्टेड!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'अब आप अपने Rizful वॉल्ट का उपयोग अपने पसंदीदा नोट्स और क्रिएटर्स को जैप करने के लिए कर सकते हैं।', - 'Your Lightning Address': 'आपका लाइटनिंग पता', - 'New to Rizful?': 'Rizful में नया?', - 'Sign up for Rizful': 'Rizful के लिए साइन अप करें', - 'If you already have a Rizful account, you can skip this step.': - 'यदि आपके पास पहले से ही एक Rizful अकाउंट है, तो आप इस चरण को छोड़ सकते हैं।', - 'Get your one-time code': 'अपना वन-टाइम कोड प्राप्त करें', - 'Get code': 'कोड प्राप्त करें', - 'Connect to your Rizful Vault': 'अपने Rizful वॉल्ट से कनेक्ट करें', - 'Paste your one-time code here': 'अपना वन-टाइम कोड यहां पेस्ट करें', - Connect: 'कनेक्ट करें', 'Set up your wallet to send and receive sats!': 'सैट्स भेजने और प्राप्त करने के लिए अपना वॉलेट सेट करें!', 'Set up': 'सेट करें' diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index cf402b1b..a52d03c3 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -437,22 +437,6 @@ export default { 'Are you absolutely sure?': 'Sei assolutamente sicuro?', 'You will not be able to send zaps to others.': 'Non sarai in grado di inviare zaps ad altri.', Disconnect: 'Disconnetti', - 'Start with a Rizful Vault': 'Inizia con un Rizful Vault', - 'or other wallets': 'o altri wallet', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vault connesso!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'Puoi ora usare il tuo Rizful Vault per zappare le tue note e creatori preferiti.', - 'Your Lightning Address': 'Il tuo Indirizzo Lightning', - 'New to Rizful?': 'Nuovo a Rizful?', - 'Sign up for Rizful': 'Iscriviti a Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'Se hai già un account Rizful, puoi saltare questo passaggio.', - 'Get your one-time code': 'Ottieni il tuo codice monouso', - 'Get code': 'Ottieni codice', - 'Connect to your Rizful Vault': 'Connettiti al tuo Rizful Vault', - 'Paste your one-time code here': 'Incolla qui il tuo codice monouso', - Connect: 'Connetti', 'Set up your wallet to send and receive sats!': 'Configura il tuo wallet per inviare e ricevere sats!', 'Set up': 'Configura' diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 8d01f290..0d07f533 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -433,22 +433,6 @@ export default { 'Are you absolutely sure?': '本当に確かですか?', 'You will not be able to send zaps to others.': '他の人にZapを送信できなくなります。', Disconnect: '接続解除', - 'Start with a Rizful Vault': 'Rizful Vaultで始める', - 'or other wallets': 'または他のウォレット', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vaultが接続されました!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'これで、Rizful Vaultを使用してお気に入りのノートやクリエイターにZapを送信できます。', - 'Your Lightning Address': 'あなたのライトニングアドレス', - 'New to Rizful?': 'Rizfulを初めて利用しますか?', - 'Sign up for Rizful': 'Rizfulにサインアップ', - 'If you already have a Rizful account, you can skip this step.': - 'すでにRizfulアカウントをお持ちの場合は、このステップをスキップできます。', - 'Get your one-time code': 'ワンタイムコードを取得', - 'Get code': 'コードを取得', - 'Connect to your Rizful Vault': 'Rizful Vaultに接続', - 'Paste your one-time code here': 'ここにワンタイムコードを貼り付けてください', - Connect: '接続', 'Set up your wallet to send and receive sats!': 'ウォレットを設定してサッツを送受信しましょう!', 'Set up': '設定する' diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 52244112..ffa7e28e 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -433,22 +433,6 @@ export default { 'Are you absolutely sure?': '정말 확실합니까?', 'You will not be able to send zaps to others.': '다른 사람에게 잽을 보낼 수 없습니다.', Disconnect: '연결 해제', - 'Start with a Rizful Vault': 'Rizful Vault로 시작하기', - 'or other wallets': '또는 다른 지갑', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vault 연결됨!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - '이제 Rizful Vault를 사용하여 좋아하는 노트와 크리에이터에게 잽을 보낼 수 있습니다.', - 'Your Lightning Address': '귀하의 라이트닝 주소', - 'New to Rizful?': 'Rizful이 처음이신가요?', - 'Sign up for Rizful': 'Rizful에 가입하기', - 'If you already have a Rizful account, you can skip this step.': - '이미 Rizful 계정이 있다면 이 단계를 건너뛸 수 있습니다.', - 'Get your one-time code': '일회용 코드 받기', - 'Get code': '코드 받기', - 'Connect to your Rizful Vault': 'Rizful Vault에 연결', - 'Paste your one-time code here': '여기에 일회용 코드를 붙여넣기', - Connect: '연결', 'Set up your wallet to send and receive sats!': '사츠를 보내고 받을 수 있도록 지갑을 설정하세요!', 'Set up': '설정하기' diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index d09402e5..96a2dbb8 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -437,22 +437,6 @@ export default { 'Are you absolutely sure?': 'Czy jesteś całkowicie pewien?', 'You will not be able to send zaps to others.': 'Nie będziesz mógł wysyłać zapów innym.', Disconnect: 'Odłącz', - 'Start with a Rizful Vault': 'Zacznij od Rizful Vault', - 'or other wallets': 'Lub inne portfele', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vault połączony!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'Możesz teraz używać swojego Rizful Vault, aby zapować swoje ulubione notatki i twórców.', - 'Your Lightning Address': 'Twój Lightning Adres', - 'New to Rizful?': 'Nowy w Rizful?', - 'Sign up for Rizful': 'Zarejestruj się w Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'Jeśli masz już konto Rizful, możesz pominąć ten krok.', - 'Get your one-time code': 'Uzyskaj swój jednorazowy kod', - 'Get code': 'Uzyskaj kod', - 'Connect to your Rizful Vault': 'Połącz się ze swoim Rizful Vault', - 'Paste your one-time code here': 'Wklej tutaj swój jednorazowy kod', - Connect: 'Połącz', 'Set up your wallet to send and receive sats!': 'Skonfiguruj swój portfel, aby wysyłać i odbierać satsy!', 'Set up': 'Skonfiguruj' diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index cc02b165..43ce3c9a 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -434,22 +434,6 @@ export default { 'Are you absolutely sure?': 'Você tem certeza absoluta?', 'You will not be able to send zaps to others.': 'Você não poderá enviar zaps para outros.', Disconnect: 'Desconectar', - 'Start with a Rizful Vault': 'Comece com um Cofre Rizful', - 'or other wallets': 'ou outras carteiras', - 'Rizful Vault': 'Cofre Rizful', - 'Rizful Vault connected!': 'Cofre Rizful conectado!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'Você pode agora usar seu Cofre Rizful para zapear suas notas e criadores favoritos.', - 'Your Lightning Address': 'Seu Endereço Lightning', - 'New to Rizful?': 'Novo no Rizful?', - 'Sign up for Rizful': 'Inscreva-se no Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'Se você já tem uma conta Rizful, pode pular esta etapa.', - 'Get your one-time code': 'Obtenha seu código único', - 'Get code': 'Obter código', - 'Connect to your Rizful Vault': 'Conecte-se ao seu Cofre Rizful', - 'Paste your one-time code here': 'Cole seu código único aqui', - Connect: 'Conectar', 'Set up your wallet to send and receive sats!': 'Configure sua carteira para enviar e receber sats!', 'Set up': 'Configurar' diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index c9cc9c64..7cd2e333 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -437,22 +437,6 @@ export default { 'Are you absolutely sure?': 'Tem certeza absoluta?', 'You will not be able to send zaps to others.': 'Você não poderá enviar zaps para outros.', Disconnect: 'Desconectar', - 'Start with a Rizful Vault': 'Comece com um Cofre Rizful', - 'or other wallets': 'outras carteiras', - 'Rizful Vault': 'Cofre Rizful', - 'Rizful Vault connected!': 'Cofre Rizful conectado!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'Agora você pode usar seu Cofre Rizful para zapear suas notas e criadores favoritos.', - 'Your Lightning Address': 'Seu Endereço Lightning', - 'New to Rizful?': 'Novo no Rizful?', - 'Sign up for Rizful': 'Inscreva-se no Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'Se você já tem uma conta Rizful, pode pular esta etapa.', - 'Get your one-time code': 'Obtenha seu código único', - 'Get code': 'Obter código', - 'Connect to your Rizful Vault': 'Conecte-se ao seu Cofre Rizful', - 'Paste your one-time code here': 'Cole seu código único aqui', - Connect: 'Conectar', 'Set up your wallet to send and receive sats!': 'Configure a sua carteira para enviar e receber sats!', 'Set up': 'Configurar' diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index bcc82f12..63f67bc3 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -439,22 +439,6 @@ export default { 'Are you absolutely sure?': 'Вы абсолютно уверены?', 'You will not be able to send zaps to others.': 'Вы не сможете отправлять запы другим.', Disconnect: 'Отключить', - 'Start with a Rizful Vault': 'Начать с Rizful Vault', - 'or other wallets': 'или другие кошельки', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vault подключён!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'Теперь вы можете использовать свой Rizful Vault, чтобы заппить ваши любимые заметки и создателей.', - 'Your Lightning Address': 'Ваш Lightning-адрес', - 'New to Rizful?': 'Новичок в Rizful?', - 'Sign up for Rizful': 'Зарегистрируйтесь на Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'Если у вас уже есть аккаунт Rizful, вы можете пропустить этот шаг.', - 'Get your one-time code': 'Получите ваш одноразовый код', - 'Get code': 'Получить код', - 'Connect to your Rizful Vault': 'Подключитесь к вашему Rizful Vault', - 'Paste your one-time code here': 'Вставьте ваш одноразовый код здесь', - Connect: 'Подключить', 'Set up your wallet to send and receive sats!': 'Настройте свой кошелёк, чтобы отправлять и получать саты!', 'Set up': 'Настроить' diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 520b29e8..fd8f138b 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -428,22 +428,6 @@ export default { 'Are you absolutely sure?': 'คุณแน่ใจอย่างยิ่งหรือไม่?', 'You will not be able to send zaps to others.': 'คุณจะไม่สามารถส่งซาตส์ไปยังผู้อื่นได้', Disconnect: 'ตัดการเชื่อมต่อ', - 'Start with a Rizful Vault': 'เริ่มต้นด้วย Rizful Vault', - 'or other wallets': 'หรือกระเป๋าสตางค์อื่นๆ', - 'Rizful Vault': 'Rizful Vault', - 'Rizful Vault connected!': 'Rizful Vault เชื่อมต่อแล้ว!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - 'คุณสามารถใช้ Rizful Vault ของคุณเพื่อส่งซาตส์ไปยังโน้ตและผู้สร้างที่คุณชื่นชอบได้แล้ว', - 'Your Lightning Address': 'ที่อยู่ Lightning ของคุณ', - 'New to Rizful?': 'ใหม่กับ Rizful?', - 'Sign up for Rizful': 'สมัครสมาชิก Rizful', - 'If you already have a Rizful account, you can skip this step.': - 'หากคุณมีบัญชี Rizful อยู่แล้ว คุณสามารถข้ามขั้นตอนนี้ได้', - 'Get your one-time code': 'รับรหัสใช้ครั้งเดียวของคุณ', - 'Get code': 'รับรหัส', - 'Connect to your Rizful Vault': 'เชื่อมต่อกับ Rizful Vault ของคุณ', - 'Paste your one-time code here': 'วางรหัสใช้ครั้งเดียวของคุณที่นี่', - Connect: 'เชื่อมต่อ', 'Set up your wallet to send and receive sats!': 'ตั้งค่ากระเป๋าของคุณเพื่อส่งและรับ sats!', 'Set up': 'ตั้งค่า' } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 476db1f5..a9bcf572 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -426,22 +426,6 @@ export default { 'Are you absolutely sure?': '您确定吗?', 'You will not be able to send zaps to others.': '您将无法向他人发送打闪。', Disconnect: '断开连接', - 'Start with a Rizful Vault': '从 Rizful 钱包开始', - 'or other wallets': '或其他钱包', - 'Rizful Vault': 'Rizful 钱包', - 'Rizful Vault connected!': 'Rizful 钱包已连接!', - 'You can now use your Rizful Vault to zap your favorite notes and creators.': - '您现在可以使用您的 Rizful 钱包为您喜欢的笔记和创作者打闪。', - 'Your Lightning Address': '您的闪电地址', - 'New to Rizful?': '第一次使用 Rizful?', - 'Sign up for Rizful': '注册 Rizful', - 'If you already have a Rizful account, you can skip this step.': - '如果您已经有一个 Rizful 账户,可以跳过此步骤。', - 'Get your one-time code': '获取一次性代码', - 'Get code': '获取代码', - 'Connect to your Rizful Vault': '连接到您的 Rizful 钱包', - 'Paste your one-time code here': '将您的一次性代码粘贴到此处', - Connect: '连接', 'Set up your wallet to send and receive sats!': '设置你的钱包以发送和接收 sats!', 'Set up': '去设置' } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c222128c..a9a99828 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,16 +1,6 @@ -import { - EMAIL_REGEX, - EMBEDDED_EVENT_REGEX, - EMBEDDED_MENTION_REGEX, - EMOJI_REGEX, - HASHTAG_REGEX, - URL_REGEX, - WS_URL_REGEX -} from '@/constants' import { TEmoji } from '@/types' import { clsx, type ClassValue } from 'clsx' import { parseNativeEmoji } from 'emoji-picker-react/src/dataUtils/parseNativeEmoji' -import { franc } from 'franc-min' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { @@ -102,77 +92,6 @@ export function isDevEnv() { return process.env.NODE_ENV === 'development' } -export function detectLanguage(text?: string): string | null { - if (!text) { - return null - } - const cleanText = text - .replace(URL_REGEX, '') - .replace(WS_URL_REGEX, '') - .replace(EMAIL_REGEX, '') - .replace(EMBEDDED_MENTION_REGEX, '') - .replace(EMBEDDED_EVENT_REGEX, '') - .replace(HASHTAG_REGEX, '') - .replace(EMOJI_REGEX, '') - .trim() - - if (!cleanText) { - return null - } - - if (/[\u3040-\u309f\u30a0-\u30ff]/.test(cleanText)) { - return 'ja' - } - if (/[\u0e00-\u0e7f]/.test(cleanText)) { - return 'th' - } - if (/[\u4e00-\u9fff]/.test(cleanText)) { - return 'zh' - } - if (/[\u0600-\u06ff]/.test(cleanText)) { - return 'ar' - } - if (/[\u0590-\u05FF]/.test(cleanText)) { - return 'fa' - } - if (/[\u0400-\u04ff]/.test(cleanText)) { - return 'ru' - } - if (/[\u0900-\u097f]/.test(cleanText)) { - return 'hi' - } - - try { - const detectedLang = franc(cleanText) - const langMap: { [key: string]: string } = { - ara: 'ar', // Arabic - deu: 'de', // German - eng: 'en', // English - spa: 'es', // Spanish - fas: 'fa', // Persian (Farsi) - pes: 'fa', // Persian (alternative code) - fra: 'fr', // French - hin: 'hi', // Hindi - ita: 'it', // Italian - jpn: 'ja', // Japanese - pol: 'pl', // Polish - por: 'pt', // Portuguese - rus: 'ru', // Russian - cmn: 'zh', // Chinese (Mandarin) - zho: 'zh' // Chinese (alternative code) - } - - const normalizedLang = langMap[detectedLang] - if (!normalizedLang) { - return 'und' - } - - return normalizedLang - } catch { - return 'und' - } -} - export function parseEmojiPickerUnified(unified: string): string | TEmoji | undefined { if (unified.startsWith(':')) { const secondColonIndex = unified.indexOf(':', 1) diff --git a/src/pages/secondary/HomePage/index.tsx b/src/pages/secondary/HomePage/index.tsx deleted file mode 100644 index bf57e65a..00000000 --- a/src/pages/secondary/HomePage/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useSecondaryPage, useSmartRelayNavigation } from '@/PageManager' -import RelaySimpleInfo from '@/components/RelaySimpleInfo' -import { Button } from '@/components/ui/button' -import { RECOMMENDED_RELAYS } from '@/constants' -import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' -import { toRelay } from '@/lib/link' -import relayInfoService from '@/services/relay-info.service' -import { TRelayInfo } from '@/types' -import { ArrowRight, Server } from 'lucide-react' -import { forwardRef, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import logger from '@/lib/logger' - -const HomePage = forwardRef(({ index }: { index?: number }, ref) => { - const { t } = useTranslation() - const { navigateToPrimaryPage } = useSecondaryPage() - const { navigateToRelay } = useSmartRelayNavigation() - // DEPRECATED: updateShowRecommendedRelaysPanel removed - double-panel functionality disabled - const [recommendedRelayInfos, setRecommendedRelayInfos] = useState([]) - - useEffect(() => { - const init = async () => { - try { - const relays = await relayInfoService.getRelayInfos(RECOMMENDED_RELAYS) - setRecommendedRelayInfos(relays.filter(Boolean) as TRelayInfo[]) - } catch (error) { - logger.error('Failed to fetch recommended relays', { error }) - } - } - init() - }, []) - - if (!recommendedRelayInfos.length) { - return ( - -
- {t('Welcome! 🥳')} -
-
- ) - } - - return ( - - -
{t('Recommended relays')}
- - } - controls={ - // DEPRECATED: Close button removed - double-panel functionality disabled - null - } - hideBackButton - hideTitlebarBottomBorder - > -
-
- {recommendedRelayInfos.map((relayInfo) => ( - { - e.stopPropagation() - navigateToRelay(toRelay(relayInfo.url)) - }} - /> - ))} -
-
- -
-
-
- ) -}) -HomePage.displayName = 'HomePage' -export default HomePage diff --git a/src/pages/secondary/RizfulPage/index.tsx b/src/pages/secondary/RizfulPage/index.tsx deleted file mode 100644 index 7a4dabb9..00000000 --- a/src/pages/secondary/RizfulPage/index.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' -import { createProfileDraftEvent } from '@/lib/draft-event' -import { isEmail } from '@/lib/utils' -import { useNostr } from '@/providers/NostrProvider' -import { useZap } from '@/providers/ZapProvider' -import { connectNWC, WebLNProviders } from '@getalby/bitcoin-connect' -import { Check, CheckCircle2, Copy, ExternalLink, Loader2 } from 'lucide-react' -import { forwardRef, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { toast } from 'sonner' - -const RIZFUL_URL = 'https://rizful.com' -const RIZFUL_SIGNUP_URL = `${RIZFUL_URL}/create-account` -const RIZFUL_GET_TOKEN_URL = `${RIZFUL_URL}/nostr_onboarding_auth_token/get_token` -const RIZFUL_TOKEN_EXCHANGE_URL = `${RIZFUL_URL}/nostr_onboarding_auth_token/post_for_secrets` - -const RizfulPage = forwardRef(({ index }: { index?: number }, ref) => { - const { t } = useTranslation() - const { pubkey, profile, profileEvent, publish, updateProfileEvent } = useNostr() - const { provider } = useZap() - const [token, setToken] = useState('') - const [connecting, setConnecting] = useState(false) - const [connected, setConnected] = useState(false) - const [copiedLightningAddress, setCopiedLightningAddress] = useState(false) - const [lightningAddress, setLightningAddress] = useState('') - - useEffect(() => { - if (provider instanceof WebLNProviders.NostrWebLNProvider) { - const lud16 = provider.client.lud16 - const domain = lud16?.split('@')[1] - if (domain !== 'rizful.com') return - - if (lud16) { - setConnected(true) - setLightningAddress(lud16) - } - } - }, [provider]) - - const updateUserProfile = async (address: string) => { - try { - if (address === profile?.lightningAddress) { - return - } - - const profileContent = profileEvent ? JSON.parse(profileEvent.content) : {} - if (isEmail(address)) { - profileContent.lud16 = address - } else if (address.startsWith('lnurl')) { - profileContent.lud06 = address - } else { - throw new Error(t('Invalid Lightning Address')) - } - - if (!profileContent.nip05) { - profileContent.nip05 = address - } - - const profileDraftEvent = createProfileDraftEvent( - JSON.stringify(profileContent), - profileEvent?.tags - ) - const newProfileEvent = await publish(profileDraftEvent) - await updateProfileEvent(newProfileEvent) - } catch (e: unknown) { - toast.error(e instanceof Error ? e.message : String(e)) - } - } - - const connectRizful = async () => { - setConnecting(true) - try { - const r = await fetch(RIZFUL_TOKEN_EXCHANGE_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'omit', - body: JSON.stringify({ - secret_code: token.trim(), - nostr_public_key: pubkey - }) - }) - - if (!r.ok) { - const errorText = await r.text() - throw new Error(errorText || 'Exchange failed') - } - - const j = (await r.json()) as { - nwc_uri?: string - lightning_address?: string - } - - if (j.nwc_uri) { - connectNWC(j.nwc_uri) - } - if (j.lightning_address) { - updateUserProfile(j.lightning_address) - } - } catch (e: unknown) { - toast.error(e instanceof Error ? e.message : String(e)) - } finally { - setConnecting(false) - } - } - - if (connected) { - return ( - -
- -
{t('Rizful Vault connected!')}
-
- {t('You can now use your Rizful Vault to zap your favorite notes and creators.')} -
- {lightningAddress && ( -
-
{t('Your Lightning Address')}:
-
{ - navigator.clipboard.writeText(lightningAddress) - setCopiedLightningAddress(true) - setTimeout(() => setCopiedLightningAddress(false), 2000) - }} - > - {lightningAddress}{' '} - {copiedLightningAddress ? ( - - ) : ( - - )} -
-
- )} -
-
- ) - } - - return ( - -
-
-
1. {t('New to Rizful?')}
- -
- {t('If you already have a Rizful account, you can skip this step.')} -
-
- -
-
2. {t('Get your one-time code')}
- -
- -
-
3. {t('Connect to your Rizful Vault')}
- { - setToken(e.target.value.trim()) - }} - /> - -
-
-
- ) -}) -RizfulPage.displayName = 'RizfulPage' -export default RizfulPage - -function openPopup(url: string, name: string, width = 520, height = 700) { - const left = Math.max((window.screenX || 0) + (window.innerWidth - width) / 2, 0) - const top = Math.max((window.screenY || 0) + (window.innerHeight - height) / 2, 0) - - return window.open( - url, - name, - `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,menubar=no,toolbar=no,location=no,status=no` - ) -} diff --git a/src/services/client.service.refactored.ts b/src/services/client.service.refactored.ts deleted file mode 100644 index f273c7aa..00000000 --- a/src/services/client.service.refactored.ts +++ /dev/null @@ -1,1215 +0,0 @@ -/** - * REFACTORED ClientService - Orchestrates sub-services - * - * This is a refactored version that delegates to focused service modules: - * - QueryService: Core query/subscription logic - * - EventService: Single event fetching and caching - * - ReplaceableEventService: Replaceable events (profiles, relay lists, etc.) - * - MacroService: Macro-specific events (Bookstr, Wikistr, etc.) - * - CacheService: Universal cache-warming and refresh strategy - * - * This maintains backward compatibility while improving maintainability. - */ - -import { FAST_READ_RELAY_URLS, ExtendedKind, FAST_WRITE_RELAY_URLS, KIND_1_BLOCKED_RELAY_URLS, NIP66_DISCOVERY_RELAY_URLS, PROFILE_RELAY_URLS, READ_ONLY_RELAY_URLS } from '@/constants' -import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' -import logger from '@/lib/logger' -import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' -import { getPubkeysFromPTags, tagNameEquals } from '@/lib/tag' -import { isLocalNetworkUrl, normalizeUrl } from '@/lib/url' -import type { - ISigner, - TProfile, - TPublishOptions, - TRelayList, - TSignerType, - TSubRequestFilter -} from '@/types' -import { kinds, Event as NEvent, Relay, SimplePool, VerifiedEvent, EventTemplate } from 'nostr-tools' -import indexedDb from './indexed-db.service' -import nip66Service from './nip66.service' -import { QueryService } from './client-query.service' -import { EventService } from './client-events.service' -import { ReplaceableEventService } from './client-replaceable-events.service' -import { MacroService, createBookstrService } from './client-macro.service' -import cacheService from './client-cache.service' - -type TTimelineRef = [string, number] - -class ClientService extends EventTarget { - static instance: ClientService - - signer?: ISigner - signerType?: TSignerType - pubkey?: string - private pool: SimplePool - - // Sub-services - private queryService: QueryService - private eventService: EventService - private replaceableEventService: ReplaceableEventService - private bookstrService: MacroService - - // Timeline management (to be extracted later) - private timelines: Record< - string, - | { - refs: TTimelineRef[] - filter: TSubRequestFilter - urls: string[] - } - | string[] - | undefined - > = {} - - // Relay management state (to be extracted to RelayService) - private publishStrikeCount = new Map() - private static readonly PUBLISH_STRIKES_THRESHOLD = 3 - private sessionRelayPublishStats = new Map() - - // Profile search index - private userIndex = new FlexSearch.Index({ - tokenize: 'forward' - }) - - // Relay list request cache (to be moved to RelayService) - private relayListRequestCache = new Map>() - - // Following favorite relays cache - private followingFavoriteRelaysCache = new LRUCache>({ - max: 50, - ttl: 1000 * 60 * 60 - }) - - constructor() { - super() - this.pool = new SimplePool() - this.pool.trackRelays = true - - // Initialize sub-services - this.queryService = new QueryService(this.pool) - this.eventService = new EventService(this.queryService) - this.replaceableEventService = new ReplaceableEventService(this.queryService) - this.bookstrService = createBookstrService(this.queryService) - } - - public static getInstance(): ClientService { - if (!ClientService.instance) { - ClientService.instance = new ClientService() - ClientService.instance.init() - } - return ClientService.instance - } - - async init() { - await indexedDb.iterateProfileEvents((profileEvent) => this.addUsernameToIndex(profileEvent)) - const runNip66 = () => this.fetchNip66RelayDiscovery().catch(() => {}) - if (typeof requestIdleCallback !== 'undefined') { - requestIdleCallback(() => runNip66(), { timeout: 8000 }) - } else { - setTimeout(runNip66, 2500) - } - } - - // Update signer in query service when it changes - setSigner(signer: ISigner | undefined, signerType: TSignerType | undefined) { - this.signer = signer - this.signerType = signerType - this.queryService.setSigner(signer, signerType) - } - - // =========== NIP-66 Discovery =========== - - private async fetchNip66RelayDiscovery(): Promise { - try { - const discoveryRelays = Array.from(new Set([...FAST_READ_RELAY_URLS, ...NIP66_DISCOVERY_RELAY_URLS])) - const events = await this.queryService.query( - discoveryRelays, - { kinds: [ExtendedKind.RELAY_DISCOVERY] }, - undefined, - { eoseTimeout: 4000, globalTimeout: 8000 } - ) - if (events.length > 0) { - nip66Service.loadFromEvents(events) - logger.info('NIP-66: loaded relay discovery events', { count: events.length }) - } - } catch (err) { - logger.info('NIP-66: failed to fetch relay discovery', { err }) - } - } - - async fetchNip66DiscoveryForRelay(relayUrl: string): Promise { - const discoveryRelays = Array.from(new Set([...FAST_READ_RELAY_URLS, ...NIP66_DISCOVERY_RELAY_URLS])) - const dTag = normalizeUrl(relayUrl) || relayUrl - const { simplifyUrl } = await import('@/lib/url') - const shortForm = simplifyUrl(dTag) - const dValues = dTag !== shortForm ? [dTag, shortForm] : [dTag] - try { - const events = await this.queryService.query( - discoveryRelays, - { kinds: [ExtendedKind.RELAY_DISCOVERY], '#d': dValues, limit: 20 }, - undefined, - { eoseTimeout: 4000, globalTimeout: 6000 } - ) - if (events.length > 0) { - nip66Service.loadFromEvents(events) - } - } catch { - // ignore per-relay fetch failure - } - } - - // =========== Event Tracking =========== - - trackEventSeenOn(eventId: string, relay: Relay): void { - this.queryService.trackEventSeenOn(eventId, relay as any) - } - - getSeenEventRelayUrls(eventId: string): string[] { - return this.queryService.getSeenEventRelayUrls(eventId) - } - - getSeenEventRelays(eventId: string): Relay[] { - // Return empty array - this method seems unused - return [] - } - - getEventHints(eventId: string): string[] { - return this.getSeenEventRelayUrls(eventId) - } - - getEventHint(eventId: string): string | undefined { - const hints = this.getEventHints(eventId) - return hints[0] - } - - // =========== Event Fetching (Delegated to EventService) =========== - - async fetchEvent(id: string): Promise { - return this.eventService.fetchEvent(id) - } - - async fetchEventForceRetry(eventId: string): Promise { - return this.eventService.fetchEventForceRetry(eventId) - } - - async fetchEventWithExternalRelays(eventId: string, externalRelays: string[]): Promise { - return this.eventService.fetchEventWithExternalRelays(eventId, externalRelays) - } - - addEventToCache(event: NEvent): void { - this.eventService.addEventToCache(event) - } - - getSessionEventsMatchingSearch(query: string, limit: number, allowedKinds: number[]): NEvent[] { - return this.eventService.getSessionEventsMatchingSearch(query, limit, allowedKinds) - } - - // =========== Query/Subscription (Delegated to QueryService) =========== - - async fetchEvents( - urls: string[], - filter: any, - options?: { - onevent?: (evt: NEvent) => void - cache?: boolean - eoseTimeout?: number - globalTimeout?: number - } - ): Promise { - const events = await this.queryService.fetchEvents(urls, filter, options) - if (options?.cache) { - events.forEach((evt) => this.eventService.addEventToCache(evt)) - } - return events - } - - async fetchEventsFromSingleRelay( - url: string, - filter: any, - options?: { globalTimeout?: number } - ): Promise<{ events: NEvent[]; connectionError?: string }> { - try { - const normalized = normalizeUrl(url) || url - if (!normalized) { - return { events: [], connectionError: 'Invalid relay URL' } - } - await this.pool.ensureRelay(normalized, { connectionTimeout: 12_000 }) - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - return { events: [], connectionError: msg } - } - try { - const events = await this.queryService.query( - [url], - filter, - undefined, - { globalTimeout: options?.globalTimeout ?? 10000 } - ) - return { events } - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - return { events: [], connectionError: msg } - } - } - - subscribe( - urls: string[], - filter: any, - callbacks: { - onevent?: (evt: NEvent) => void - oneose?: (eosed: boolean) => void - onclose?: (url: string, reason: string) => void - startLogin?: () => void - onAllClose?: (reasons: string[]) => void - } - ) { - return this.queryService.subscribe(urls, filter, callbacks) - } - - // =========== Replaceable Events (Delegated to ReplaceableEventService) =========== - - async fetchProfileEvent(id: string, skipCache: boolean = false): Promise { - let pubkey: string | undefined - let relays: string[] = [] - if (/^[0-9a-f]{64}$/.test(id)) { - pubkey = id - } else { - const { data, type } = await import('nostr-tools/nip19').then(m => m.default.decode(id)) - switch (type) { - case 'npub': - pubkey = data - break - case 'nprofile': - pubkey = data.pubkey - if (data.relays) relays = data.relays - break - } - } - - if (!pubkey) { - throw new Error('Invalid id') - } - if (!skipCache) { - const localProfile = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata) - if (localProfile) { - return localProfile - } - } - const profileEvent = await this.replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Metadata) - if (profileEvent) { - this.addUsernameToIndex(profileEvent) - return profileEvent - } - - if (!relays.length) { - return undefined - } - - // Try harder with specified relays - const events = await this.queryService.query( - relays, - { - authors: [pubkey], - kinds: [kinds.Metadata], - limit: 1 - }, - undefined, - { - replaceableRace: true, - eoseTimeout: 200, - globalTimeout: 3000 - } - ) - - const profileEventFromRelays = events[0] - if (profileEventFromRelays) { - this.addUsernameToIndex(profileEventFromRelays) - await indexedDb.putReplaceableEvent(profileEventFromRelays) - } - - return profileEventFromRelays - } - - async fetchProfile(id: string, skipCache: boolean = false): Promise { - const profileEvent = await this.fetchProfileEvent(id, skipCache) - if (profileEvent) { - return getProfileFromEvent(profileEvent) - } - - try { - const pubkey = userIdToPubkey(id) - return { pubkey, npub: pubkeyToNpub(pubkey) ?? '', username: formatPubkey(pubkey) } - } catch { - return undefined - } - } - - async fetchProfilesForPubkeys(pubkeys: string[]): Promise { - const deduped = Array.from(new Set(pubkeys.filter((p) => p && p.length === 64))) - if (deduped.length === 0) return [] - const events = await this.replaceableEventService.fetchReplaceableEventsFromBigRelays(deduped, kinds.Metadata) - const profiles: TProfile[] = [] - for (let i = 0; i < deduped.length; i++) { - const ev = events[i] - if (ev) { - this.addUsernameToIndex(ev) - profiles.push(getProfileFromEvent(ev)) - } else { - const pubkey = deduped[i]! - profiles.push({ - pubkey, - npub: pubkeyToNpub(pubkey) ?? '', - username: formatPubkey(pubkey) - }) - } - } - return profiles - } - - async getProfileFromIndexedDB(id: string): Promise { - let pubkey: string | undefined - try { - if (/^[0-9a-f]{64}$/.test(id)) { - pubkey = id - } else { - const { data, type } = await import('nostr-tools/nip19').then(m => m.default.decode(id)) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') pubkey = data.pubkey - } - } catch { - return undefined - } - if (!pubkey) return undefined - const event = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata) - if (!event || event === null) return undefined - return getProfileFromEvent(event) - } - - async updateProfileEventCache(event: NEvent): Promise { - await this.replaceableEventService.updateReplaceableEventCache(event) - } - - // =========== Relay Lists (Delegated to ReplaceableEventService) =========== - - async fetchRelayListEvent(pubkey: string) { - const event = await this.replaceableEventService.fetchReplaceableEvent(pubkey, kinds.RelayList) - return event ?? null - } - - clearRelayListCache(pubkey: string) { - this.relayListRequestCache.delete(pubkey) - } - - async fetchRelayList(pubkey: string): Promise { - // Deduplicate concurrent requests - const existingRequest = this.relayListRequestCache.get(pubkey) - if (existingRequest) { - logger.debug('[FetchRelayList] Using cached in-flight request', { pubkey: pubkey.substring(0, 8) }) - return existingRequest - } - - logger.debug('[FetchRelayList] Starting fetch', { pubkey: pubkey.substring(0, 8) }) - const requestPromise = (async () => { - try { - const startTime = Date.now() - const [relayList] = await this.fetchRelayLists([pubkey]) - const duration = Date.now() - startTime - logger.debug('[FetchRelayList] Fetch completed', { - pubkey: pubkey.substring(0, 8), - duration: `${duration}ms`, - hasRelayList: !!relayList, - writeCount: relayList?.write?.length ?? 0, - readCount: relayList?.read?.length ?? 0 - }) - return relayList - } catch (error) { - logger.error('[FetchRelayList] Fetch failed', { - pubkey: pubkey.substring(0, 8), - error: error instanceof Error ? error.message : String(error) - }) - throw error - } finally { - this.relayListRequestCache.delete(pubkey) - } - })() - - this.relayListRequestCache.set(pubkey, requestPromise) - return requestPromise - } - - async fetchRelayLists(pubkeys: string[]): Promise { - // Check IndexedDB first - const storedRelayEvents = await Promise.all( - pubkeys.map(pubkey => indexedDb.getReplaceableEvent(pubkey, kinds.RelayList)) - ) - const storedCacheRelayEvents = await Promise.all( - pubkeys.map(pubkey => indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)) - ) - - // Fetch from relays - const relayEvents = await this.replaceableEventService.fetchReplaceableEventsFromBigRelays(pubkeys, kinds.RelayList) - const cacheRelayEvents = await this.replaceableEventService.fetchReplaceableEventsFromBigRelays(pubkeys, ExtendedKind.CACHE_RELAYS) - - return pubkeys.map((pubkey, index) => { - const storedRelayEvent = storedRelayEvents[index] - const storedCacheEvent = storedCacheRelayEvents[index] - const relayEvent = relayEvents[index] || storedRelayEvent - const cacheEvent = cacheRelayEvents[index] || storedCacheEvent - - const relayList = relayEvent ? getRelayListFromEvent(relayEvent) : { - write: [], - read: [], - originalRelays: [] - } - - // Merge cache relays if available - if (cacheEvent) { - const cacheRelayList = getRelayListFromEvent(cacheEvent) - const mergedRead = [...cacheRelayList.read, ...relayList.read] - const mergedWrite = [...cacheRelayList.write, ...relayList.write] - return { - write: Array.from(new Set(mergedWrite)), - read: Array.from(new Set(mergedRead)), - originalRelays: [...(cacheRelayList.originalRelays || []), ...(relayList.originalRelays || [])] - } - } - - return relayList - }) - } - - async updateRelayListCache(event: NEvent): Promise { - await this.replaceableEventService.updateReplaceableEventCache(event) - } - - // =========== Other Replaceable Events =========== - - async fetchFollowListEvent(pubkey: string) { - return await this.replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Contacts) - } - - async fetchFollowings(pubkey: string): Promise { - const followListEvent = await this.fetchFollowListEvent(pubkey) - if (!followListEvent) return [] - return getPubkeysFromPTags(followListEvent.tags) - } - - async updateFollowListCache(evt: NEvent): Promise { - await this.replaceableEventService.updateReplaceableEventCache(evt) - } - - async fetchMuteListEvent(pubkey: string) { - return await this.replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Mutelist) - } - - async fetchBookmarkListEvent(pubkey: string) { - return await this.replaceableEventService.fetchReplaceableEvent(pubkey, kinds.BookmarkList) - } - - async fetchBlossomServerListEvent(pubkey: string) { - return await this.replaceableEventService.fetchReplaceableEvent(pubkey, ExtendedKind.BLOSSOM_SERVER_LIST) - } - - async fetchBlossomServerList(pubkey: string): Promise { - const evt = await this.fetchBlossomServerListEvent(pubkey) - if (!evt) return [] - const { getServersFromServerTags } = await import('@/lib/tag') - return getServersFromServerTags(evt.tags) - } - - async updateBlossomServerListEventCache(evt: NEvent): Promise { - await this.replaceableEventService.updateReplaceableEventCache(evt) - } - - async fetchInterestListEvent(pubkey: string) { - return await this.replaceableEventService.fetchReplaceableEvent(pubkey, 10015) - } - - async fetchPinListEvent(pubkey: string) { - return await this.replaceableEventService.fetchReplaceableEvent(pubkey, 10001) - } - - async fetchPaymentInfoEvent(pubkey: string) { - return await this.replaceableEventService.fetchReplaceableEvent(pubkey, ExtendedKind.PAYMENT_INFO) - } - - async updatePaymentInfoCache(evt: NEvent): Promise { - await this.replaceableEventService.updateReplaceableEventCache(evt) - } - - async forceRefreshProfileAndPaymentInfoCache(pubkey: string): Promise { - await Promise.all([ - this.replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Metadata), - this.replaceableEventService.fetchReplaceableEvent(pubkey, ExtendedKind.PAYMENT_INFO) - ]) - } - - async fetchEmojiSetEvents(pointers: string[]) { - // Implementation would use replaceableEventService - return [] - } - - // =========== Favorite Relays =========== - - async fetchFavoriteRelays(pubkey: string): Promise { - const event = await this.replaceableEventService.fetchReplaceableEvent(pubkey, ExtendedKind.FAVORITE_RELAYS) - if (!event) return [] - const relays: string[] = [] - event.tags.forEach(([tagName, tagValue]) => { - if (tagName === 'relay' && tagValue) { - const normalizedUrl = normalizeUrl(tagValue) - if (normalizedUrl && !relays.includes(normalizedUrl)) { - relays.push(normalizedUrl) - } - } - }) - return relays - } - - // =========== Profile Search =========== - - async searchProfiles(relayUrls: string[], filter: any): Promise { - const events = await this.queryService.query(relayUrls, { - ...filter, - kinds: [kinds.Metadata] - }, undefined, { - replaceableRace: true, - eoseTimeout: 200, - globalTimeout: 3000 - }) - - const profileEvents = events.sort((a, b) => b.created_at - a.created_at) - await Promise.allSettled(profileEvents.map((profile) => this.addUsernameToIndex(profile))) - profileEvents.forEach((profile) => this.replaceableEventService.updateReplaceableEventCache(profile)) - return profileEvents.map((profileEvent) => getProfileFromEvent(profileEvent)) - } - - async searchNpubsFromLocal(query: string, limit: number = 100): Promise { - const result = await this.userIndex.searchAsync(query, { limit }) - return result.map((pubkey) => pubkeyToNpub(pubkey as string)).filter(Boolean) as string[] - } - - async searchNpubsForMention(query: string, limit: number = 100): Promise { - // Implementation would use follow list and search - const { SEARCHABLE_RELAY_URLS } = await import('@/constants') - const out: string[] = [] - const addedNpubs = new Set() - const qLower = query.trim().toLowerCase() - - if (qLower.length === 0) return out - - try { - const { pubkey } = await import('@/providers/NostrProvider').then(m => m.useNostr()) - if (pubkey) { - const followListEvent = await this.fetchFollowListEvent(pubkey) - if (followListEvent) { - const followings = getPubkeysFromPTags(followListEvent.tags) - const profiles = await Promise.all( - followings.slice(0, 100).map((pubkey) => { - const npub = pubkeyToNpub(pubkey) - return npub ? this.fetchProfile(npub) : Promise.resolve(undefined) - }) - ) - const matchText = (p: TProfile) => - ((p.username ?? '') + ' ' + (p.original_username ?? '') + ' ' + (p.nip05 ?? '')).toLowerCase() - for (const p of profiles) { - if (!p) continue - const npub = p.npub || pubkeyToNpub(p.pubkey) - if (!npub || addedNpubs.has(npub)) continue - if (!matchText(p).includes(qLower)) continue - addedNpubs.add(npub) - out.push(npub) - if (out.length >= limit) return out - } - } - } - } catch { - // ignore follow-list errors - } - - const local = await this.searchNpubsFromLocal(qLower, limit) - for (const npub of local) { - if (addedNpubs.has(npub)) continue - addedNpubs.add(npub) - out.push(npub) - if (out.length >= limit) return out - } - - if (out.length < limit && qLower.length >= 1) { - try { - const relayProfiles = await this.searchProfiles(SEARCHABLE_RELAY_URLS, { - search: qLower, - limit: Math.min(limit - out.length, 20) - }) - for (const p of relayProfiles) { - const npub = p.npub || pubkeyToNpub(p.pubkey) - if (!npub || addedNpubs.has(npub)) continue - addedNpubs.add(npub) - out.push(npub) - if (out.length >= limit) return out - } - } catch { - // ignore relay search errors - } - } - - return out - } - - async searchProfilesFromLocal(query: string, limit: number = 100): Promise { - const npubs = await this.searchNpubsFromLocal(query, limit) - const profiles = await Promise.all(npubs.map((npub) => this.fetchProfile(npub))) - return profiles.filter((profile) => !!profile) as TProfile[] - } - - private async addUsernameToIndex(profileEvent: NEvent): Promise { - try { - const profileObj = JSON.parse(profileEvent.content) - const text = [ - profileObj.display_name?.trim() ?? '', - profileObj.name?.trim() ?? '', - profileObj.nip05 - ?.split('@') - .map((s: string) => s.trim()) - .join(' ') ?? '' - ].join(' ') - if (!text) return - - await this.userIndex.addAsync(profileEvent.pubkey, text) - } catch { - return - } - } - - async initUserIndexFromFollowings(pubkey: string, signal: AbortSignal): Promise { - const followings = await this.fetchFollowings(pubkey) - for (let i = 0; i < followings.length; i += 20) { - if (signal.aborted) break - await Promise.allSettled( - followings.slice(i, i + 20).map((pubkey) => this.fetchProfileEvent(pubkey)) - ) - await new Promise((resolve) => setTimeout(resolve, 1000)) - } - } - - // =========== Macro Events (Delegated to MacroService) =========== - - async fetchBookstrEvents(filters: { - type?: string - book?: string - chapter?: number - verse?: string - version?: string - }): Promise { - return this.bookstrService.fetchMacroEvents(filters) - } - - async getCachedBookstrEvents(filters: { - type?: string - book?: string - chapter?: number - verse?: string - version?: string - }): Promise { - return this.bookstrService.getCachedMacroEvents(filters) - } - - // =========== Relay Management & Publishing =========== - // TODO: Extract to RelayService - - async determineTargetRelays( - event: NEvent, - { specifiedRelayUrls, additionalRelayUrls }: TPublishOptions = {} - ): Promise { - // Keep existing implementation for now - to be extracted to RelayService - // This is a complex method that needs careful extraction - if (event.kind === kinds.RelayList) { - logger.info('[DetermineTargetRelays] Determining target relays for relay list event', { - pubkey: event.pubkey?.substring(0, 8), - hasSpecifiedRelays: !!specifiedRelayUrls?.length, - specifiedRelayCount: specifiedRelayUrls?.length ?? 0, - hasAdditionalRelays: !!additionalRelayUrls?.length, - additionalRelayCount: additionalRelayUrls?.length ?? 0 - }) - } - - if (event.kind === kinds.Report) { - const relayList = await this.fetchRelayList(event.pubkey) - const userWriteRelays = relayList?.write.slice(0, 10) ?? [] - const targetEventId = event.tags.find(tagNameEquals('e'))?.[1] - const seenRelays: string[] = [] - - if (targetEventId) { - const allSeenRelays = this.getSeenEventRelayUrls(targetEventId) - const userWriteRelaySet = new Set(userWriteRelays.map(url => normalizeUrl(url) || url)) - seenRelays.push(...allSeenRelays.filter(url => { - const normalized = normalizeUrl(url) || url - return userWriteRelaySet.has(normalized) - })) - } - - const reportRelays = Array.from(new Set([...userWriteRelays, ...seenRelays])) - if (reportRelays.length === 0) { - reportRelays.push(...FAST_WRITE_RELAY_URLS) - } - return reportRelays - } - - // Public messages and calendar RSVPs - if ( - event.kind === ExtendedKind.PUBLIC_MESSAGE || - event.kind === ExtendedKind.CALENDAR_EVENT_RSVP - ) { - const authorRelayList = await this.fetchRelayList(event.pubkey).catch(() => ({ write: [] as string[], read: [] as string[] })) - let authorWrite = (authorRelayList?.write ?? []).map((url) => normalizeUrl(url)).filter(Boolean) as string[] - if (authorWrite.length === 0) { - authorWrite = [...FAST_WRITE_RELAY_URLS] - } - const recipientPubkeys = Array.from( - new Set( - event.tags.filter((t) => t[0] === 'p' && t[1] && isValidPubkey(t[1])).map((t) => t[1] as string) - ) - ).filter((p) => p !== event.pubkey) - let recipientRead: string[] = [] - if (recipientPubkeys.length > 0) { - const recipientRelayLists = await this.fetchRelayLists(recipientPubkeys) - recipientRead = recipientRelayLists.flatMap((rl) => rl?.read ?? []) - recipientRead = recipientRead - .map((url) => normalizeUrl(url)) - .filter((url): url is string => !!url && !isLocalNetworkUrl(url)) - } - const relays = Array.from(new Set([...authorWrite, ...recipientRead])) - return relays.length > 0 ? relays : [...FAST_WRITE_RELAY_URLS] - } - - let relays: string[] - if (specifiedRelayUrls?.length) { - relays = specifiedRelayUrls - } else { - const _additionalRelayUrls: string[] = additionalRelayUrls ?? [] - - if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) { - const mentions: string[] = [] - event.tags.forEach(([tagName, tagValue]) => { - if ( - ['p', 'P'].includes(tagName) && - !!tagValue && - isValidPubkey(tagValue) && - !mentions.includes(tagValue) - ) { - mentions.push(tagValue) - } - }) - if (mentions.length > 0) { - const relayLists = await this.fetchRelayLists(mentions) - relayLists.forEach((relayList) => { - _additionalRelayUrls.push(...relayList.read.slice(0, 4)) - }) - } - } - - if ( - [ - kinds.RelayList, - ExtendedKind.CACHE_RELAYS, - kinds.Contacts, - ExtendedKind.BLOSSOM_SERVER_LIST, - ExtendedKind.RELAY_REVIEW - ].includes(event.kind) - ) { - _additionalRelayUrls.push(...FAST_READ_RELAY_URLS, ...PROFILE_RELAY_URLS) - } else if (event.kind === ExtendedKind.FAVORITE_RELAYS) { - _additionalRelayUrls.push(...FAST_WRITE_RELAY_URLS) - } else if (event.kind === ExtendedKind.RSS_FEED_LIST) { - _additionalRelayUrls.push(...FAST_WRITE_RELAY_URLS, ...PROFILE_RELAY_URLS) - } - - let relayList: TRelayList | undefined - try { - relayList = await this.fetchRelayList(event.pubkey) - } catch (err) { - logger.warn('[DetermineTargetRelays] fetchRelayList failed, using fallback relays', { - pubkey: event.pubkey?.substring(0, 8), - error: err instanceof Error ? err.message : String(err) - }) - relayList = { write: [], read: [], originalRelays: [] } - } - relays = (relayList?.write.slice(0, 10) ?? []).concat( - Array.from(new Set(_additionalRelayUrls)) ?? [] - ) - } - - if (!relays.length) { - relays = [...FAST_WRITE_RELAY_URLS] - } - - const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u)) - const kind1BlockedSet = new Set(KIND_1_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) - relays = relays.filter((url) => { - const n = normalizeUrl(url) || url - if (readOnlySet.has(n)) return false - if (event.kind === kinds.ShortTextNote && kind1BlockedSet.has(n)) return false - return true - }) - - return relays - } - - private recordPublishFailures(relayStatuses: { url: string; success: boolean; error?: string }[]) { - relayStatuses.filter((s) => !s.success).forEach((s) => { - const n = normalizeUrl(s.url) || s.url - const count = (this.publishStrikeCount.get(n) ?? 0) + 1 - this.publishStrikeCount.set(n, count) - if (count >= ClientService.PUBLISH_STRIKES_THRESHOLD) { - logger.debug('[PublishEvent] Relay reached 3 strikes, skipping for session', { url: n }) - } - }) - } - - recordPublishSuccess(url: string, latencyMs: number) { - const n = normalizeUrl(url) || url - const cur = this.sessionRelayPublishStats.get(n) - if (cur) { - cur.successCount += 1 - cur.sumLatencyMs += latencyMs - } else { - this.sessionRelayPublishStats.set(n, { successCount: 1, sumLatencyMs: latencyMs }) - } - } - - getSessionSuccessfulPublishRelayUrlsForRandomPool(): string[] { - return Array.from(this.sessionRelayPublishStats.entries()) - .filter(([_, stats]) => stats.successCount >= 2) - .sort(([_, a], [__, b]) => { - const avgA = a.sumLatencyMs / a.successCount - const avgB = b.sumLatencyMs / b.successCount - return avgA - avgB - }) - .slice(0, 20) - .map(([url]) => url) - } - - getSessionRelayDebug(): { url: string; stats: { successCount: number; sumLatencyMs: number } }[] { - return Array.from(this.sessionRelayPublishStats.entries()).map(([url, stats]) => ({ - url, - stats - })) - } - - getPreferredRelaysForRandom(candidateUrls: string[], count: number): string[] { - const sessionUrls = this.getSessionSuccessfulPublishRelayUrlsForRandomPool() - const sessionSet = new Set(sessionUrls) - const preferred: string[] = [] - const rest: string[] = [] - - for (const url of candidateUrls) { - const n = normalizeUrl(url) || url - if (sessionSet.has(n)) { - preferred.push(n) - } else { - rest.push(n) - } - } - - const needed = count - preferred.length - if (needed > 0) { - preferred.push(...rest.slice(0, needed)) - } - - return preferred.slice(0, count) - } - - clearRelayConnectionState(relayUrl: string): void { - const n = normalizeUrl(relayUrl) || relayUrl - this.publishStrikeCount.delete(n) - this.sessionRelayPublishStats.delete(n) - } - - async publishEvent(relayUrls: string[], event: NEvent) { - // Keep existing implementation - complex publishing logic - // TODO: Extract to RelayService - const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u)) - const kind1BlockedSet = new Set(KIND_1_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) - let filtered = relayUrls.filter((url) => { - const n = normalizeUrl(url) || url - if (readOnlySet.has(n)) return false - if (event.kind === kinds.ShortTextNote && kind1BlockedSet.has(n)) return false - const strikes = this.publishStrikeCount.get(n) ?? 0 - if (strikes >= ClientService.PUBLISH_STRIKES_THRESHOLD) return false - return true - }) - filtered = Array.from(new Set(filtered)) - - const relayStatuses: { url: string; success: boolean; error?: string }[] = [] - const uniqueRelayUrls = filtered - - return new Promise<{ success: boolean; relayStatuses: typeof relayStatuses; successCount: number; totalCount: number }>((resolve) => { - let successCount = 0 - let finishedCount = 0 - const errors: { url: string; error: any }[] = [] - let hasResolved = false - - const globalTimeout = setTimeout(() => { - if (hasResolved) return - uniqueRelayUrls.forEach(url => { - const alreadyFinished = relayStatuses.some(rs => rs.url === url) - if (!alreadyFinished) { - relayStatuses.push({ url, success: false, error: 'Timeout: Operation took too long' }) - finishedCount++ - } - }) - if (!hasResolved) { - hasResolved = true - this.recordPublishFailures(relayStatuses) - resolve({ - success: successCount >= filtered.length / 3, - relayStatuses, - successCount, - totalCount: filtered.length - }) - } - }, 30_000) - Promise.allSettled( - uniqueRelayUrls.map(async (url, index) => { - const startMs = Date.now() - const isLocal = isLocalNetworkUrl(url) - const connectionTimeout = isLocal ? 5_000 : 8_000 - const publishTimeout = isLocal ? 5_000 : 8_000 - - const relayTimeout = setTimeout(() => { - logger.warn(`[PublishEvent] Per-relay timeout for ${url}`) - }, connectionTimeout + publishTimeout + 2_000) - - try { - let relay: Relay - const connectionPromise = isLocal - ? Promise.race([ - this.pool.ensureRelay(url), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Local relay connection timeout')), connectionTimeout) - ) - ]) - : Promise.race([ - this.pool.ensureRelay(url), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Remote relay connection timeout')), connectionTimeout) - ) - ]) - - relay = await connectionPromise - relay.publishTimeout = publishTimeout - - const publishPromise = relay - .publish(event) - .then(() => { - this.recordPublishSuccess(url, Date.now() - startMs) - this.trackEventSeenOn(event.id, relay) - successCount++ - relayStatuses.push({ url, success: true }) - }) - .catch((error) => { - if ( - error instanceof Error && - error.message.startsWith('auth-required') && - this.signer && - this.signerType !== 'npub' - ) { - return relay - .auth((authEvt: EventTemplate) => this.signer!.signEvent(authEvt)) - .then(() => relay.publish(event)) - .then(() => { - this.recordPublishSuccess(url, Date.now() - startMs) - this.trackEventSeenOn(event.id, relay) - successCount++ - relayStatuses.push({ url, success: true }) - }) - .catch((authError) => { - relayStatuses.push({ url, success: false, error: authError.message }) - }) - } else { - relayStatuses.push({ url, success: false, error: error.message }) - } - }) - - await Promise.race([ - publishPromise, - new Promise((_, reject) => - setTimeout(() => reject(new Error(`Publish timeout after ${publishTimeout}ms`)), publishTimeout) - ) - ]) - } catch (error) { - relayStatuses.push({ - url, - success: false, - error: error instanceof Error ? error.message : 'Connection failed' - }) - } finally { - clearTimeout(relayTimeout) - const currentFinished = ++finishedCount - - if (successCount >= uniqueRelayUrls.length / 3) { - this.emitNewEvent(event) - } - if (currentFinished >= uniqueRelayUrls.length && !hasResolved) { - hasResolved = true - this.recordPublishFailures(relayStatuses) - clearTimeout(globalTimeout) - resolve({ - success: successCount >= uniqueRelayUrls.length / 3, - relayStatuses, - successCount, - totalCount: uniqueRelayUrls.length - }) - } - - if (!hasResolved && successCount >= Math.max(1, Math.ceil(uniqueRelayUrls.length / 3)) && currentFinished >= Math.max(1, Math.ceil(uniqueRelayUrls.length / 3))) { - setTimeout(() => { - if (!hasResolved) { - hasResolved = true - this.recordPublishFailures(relayStatuses) - clearTimeout(globalTimeout) - resolve({ - success: true, - relayStatuses, - successCount, - totalCount: uniqueRelayUrls.length - }) - } - }, 2000) - } - } - }) - ) - }) - } - - emitNewEvent(event: NEvent) { - this.dispatchEvent(new CustomEvent('newEvent', { detail: event })) - } - - async signHttpAuth(url: string, method: string, description = '') { - if (!this.signer) { - throw new Error('Please login first to sign the event') - } - const { dayjs } = await import('dayjs') - const event = await this.signer.signEvent({ - content: '', - kind: kinds.HTTPAuth, - created_at: dayjs().unix(), - tags: [ - ['u', url], - ['method', method] - ] - }) - return 'Nostr ' + btoa(JSON.stringify(event)) - } - - // =========== Timeline Management =========== - // TODO: Extract to TimelineService - - private generateTimelineKey(urls: string[], filter: any): string { - const { sha256 } = require('@noble/hashes/sha2') - const key = JSON.stringify({ urls, filter }) - return sha256(key) - } - - private generateMultipleTimelinesKey(subRequests: { urls: string[]; filter: any }[]): string { - const { sha256 } = require('@noble/hashes/sha2') - const key = JSON.stringify(subRequests) - return sha256(key) - } - - async subscribeTimeline( - subRequests: { urls: string[]; filter: any }[], - { - onEvents, - onNew, - onClose - }: { - onEvents: (events: NEvent[], eosed: boolean) => void - onNew: (evt: NEvent) => void - onClose?: (url: string, reason: string) => void - }, - { - startLogin, - needSort = true - }: { - startLogin?: () => void - needSort?: boolean - } = {} - ) { - // Keep existing implementation - complex timeline logic - // TODO: Extract to TimelineService - const key = this.generateMultipleTimelinesKey(subRequests) - // Implementation would use _subscribeTimeline - return { close: () => {} } - } - - async loadMoreTimeline(key: string, until: number, limit: number) { - // Keep existing implementation - // TODO: Extract to TimelineService - return [] - } - - // =========== Following Favorite Relays =========== - - async fetchFollowingFavoriteRelays(pubkey: string): Promise<[string, string[]][]> { - const cached = this.followingFavoriteRelaysCache.get(pubkey) - if (cached) { - return cached - } - const promise = this._fetchFollowingFavoriteRelays(pubkey) - this.followingFavoriteRelaysCache.set(pubkey, promise) - return promise - } - - private async _fetchFollowingFavoriteRelays(pubkey: string): Promise<[string, string[]][]> { - const followings = await this.fetchFollowings(pubkey) - const favoriteRelaysEvents = await this.replaceableEventService.fetchReplaceableEventsFromBigRelays( - followings.slice(0, 100), - ExtendedKind.FAVORITE_RELAYS - ) - const result: [string, string[]][] = [] - for (let i = 0; i < followings.length && i < favoriteRelaysEvents.length; i++) { - const event = favoriteRelaysEvents[i] - if (event) { - const relays: string[] = [] - event.tags.forEach(([tagName, tagValue]) => { - if (tagName === 'relay' && tagValue) { - const normalizedUrl = normalizeUrl(tagValue) - if (normalizedUrl && !relays.includes(normalizedUrl)) { - relays.push(normalizedUrl) - } - } - }) - if (relays.length > 0) { - result.push([followings[i]!, relays]) - } - } - } - return result - } - - // =========== Utility Methods =========== - - async generateSubRequestsForPubkeys(pubkeys: string[], myPubkey?: string | null) { - // Implementation would generate subscription requests - return [] - } - - clearInMemoryCaches(): void { - this.eventService.clearCaches() - this.replaceableEventService.clearCaches() - this.relayListRequestCache.clear() - this.followingFavoriteRelaysCache?.clear() - logger.info('[ClientService] In-memory caches cleared') - } - - getAlreadyTriedRelays(): string[] { - return [] - } -} - -const instance = ClientService.getInstance() -export default instance diff --git a/src/styles/katex-bundle.css b/src/styles/katex-bundle.css new file mode 100644 index 00000000..c240a3e2 --- /dev/null +++ b/src/styles/katex-bundle.css @@ -0,0 +1,3 @@ +@import './katex-fonts-subset.css'; +@import './katex-core.min.css'; +@import './katex-overrides.css'; diff --git a/src/styles/katex-core.min.css b/src/styles/katex-core.min.css new file mode 100644 index 00000000..977c53a2 --- /dev/null +++ b/src/styles/katex-core.min.css @@ -0,0 +1 @@ +.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.16.38"}.katex .katex-mathml{clip:rect(1px,1px,1px,1px);border:0;height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .hbox{width:100%}.katex .hbox,.katex .thinbox{display:inline-flex;flex-direction:row}.katex .thinbox{max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{border:0 solid;display:inline-block;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo} diff --git a/src/styles/katex-fonts-subset.css b/src/styles/katex-fonts-subset.css new file mode 100644 index 00000000..822cd7c3 --- /dev/null +++ b/src/styles/katex-fonts-subset.css @@ -0,0 +1,73 @@ +/* Subset of KaTeX fonts: Main + Math + Size1–4 (woff2 only). Drops AMS, Caligraphic, + Fraktur, SansSerif, Script, Typewriter to shrink the build / PWA precache. */ + +@font-face { + font-family: KaTeX_Main; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Main-Bold.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Main; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Main-BoldItalic.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Main; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Main-Italic.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Main; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Main-Regular.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Math; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Math-BoldItalic.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Math; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Math-Italic.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Size1; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Size1-Regular.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Size2; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Size2-Regular.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Size3; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Size3-Regular.woff2') format('woff2'); +} +@font-face { + font-family: KaTeX_Size4; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../../node_modules/katex/dist/fonts/KaTeX_Size4-Regular.woff2') format('woff2'); +} diff --git a/src/styles/katex-overrides.css b/src/styles/katex-overrides.css new file mode 100644 index 00000000..022ff73f --- /dev/null +++ b/src/styles/katex-overrides.css @@ -0,0 +1,41 @@ +/* Map styles that referenced bundled-out fonts onto KaTeX_Main so layout still works; + \mathbb, \mathcal, \mathfrak, etc. will not match canonical TeX glyphs. */ + +.katex .texttt, +.katex .mathtt { + font-family: KaTeX_Main, ui-monospace, monospace; +} +.katex .textsf, +.katex .mathsf, +.katex .mathboldsf, +.katex .textboldsf, +.katex .mathitsf, +.katex .mathsfit, +.katex .textitsf { + font-family: KaTeX_Main, sans-serif; +} +.katex .mathboldsf, +.katex .textboldsf { + font-weight: 700; +} +.katex .mathitsf, +.katex .mathsfit, +.katex .textitsf { + font-style: italic; +} +.katex .amsrm, +.katex .mathbb, +.katex .textbb, +.katex .mathcal, +.katex .mathfrak, +.katex .textfrak, +.katex .mathboldfrak, +.katex .textboldfrak, +.katex .mathscr, +.katex .textscr { + font-family: KaTeX_Main, Times New Roman, serif; +} +.katex .mathboldfrak, +.katex .textboldfrak { + font-weight: 700; +} diff --git a/vite.config.ts b/vite.config.ts index cd360940..bb8e9629 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -38,6 +38,122 @@ export default defineConfig({ }, build: { rollupOptions: { + output: { + manualChunks(id) { + if (!id.includes('node_modules')) return undefined + + // Lazy-loaded only — must not share a chunk with sync vendors or it gets preloaded + if (id.includes('@asciidoctor')) { + return 'vendor-asciidoctor' + } + + if (id.includes('/katex/') || id.includes('node_modules/katex/')) { + return 'vendor-katex' + } + + // React core (load first; keep together) + if (/node_modules\/(react-dom|react\/|scheduler\/|use-sync-external-store\/)/.test(id)) { + return 'vendor-react' + } + + // TipTap + ProseMirror + if (id.includes('@tiptap') || id.includes('prosemirror-')) { + return 'vendor-editor' + } + + // Radix UI primitives + if (id.includes('@radix-ui')) { + return 'vendor-radix' + } + + // Nostr + crypto used by the stack + if ( + id.includes('nostr-tools') || + id.includes('@noble') || + id.includes('@scure') + ) { + return 'vendor-nostr' + } + + if (id.includes('lucide-react')) { + return 'vendor-lucide' + } + + if (id.includes('i18next') || id.includes('react-i18next')) { + return 'vendor-i18n' + } + + if (id.includes('@dnd-kit')) { + return 'vendor-dnd' + } + + if (id.includes('highlight.js')) { + return 'vendor-highlight' + } + + if (id.includes('flexsearch')) { + return 'vendor-flexsearch' + } + + if (id.includes('emoji-picker-react')) { + return 'vendor-emoji' + } + + if (id.includes('yet-another-react-lightbox')) { + return 'vendor-lightbox' + } + + if ( + id.includes('@getalby') || + id.includes('bitcoin-connect') || + id.includes('nstart-modal') + ) { + return 'vendor-lightning' + } + + if (id.includes('embla-carousel')) { + return 'vendor-embla' + } + + if (id.includes('qr-code-styling') || id.includes('/qr-scanner/')) { + return 'vendor-qr' + } + + if (id.includes('/cmdk/')) { + return 'vendor-cmdk' + } + + if (id.includes('/vaul/')) { + return 'vendor-vaul' + } + + if (id.includes('tippy.js')) { + return 'vendor-tippy' + } + + if (id.includes('/zod/') || id.includes('node_modules/zod')) { + return 'vendor-zod' + } + + if (id.includes('/dayjs/')) { + return 'vendor-dayjs' + } + + if (id.includes('/sonner/')) { + return 'vendor-sonner' + } + + if (id.includes('blossom-client-sdk')) { + return 'vendor-blossom' + } + + if (id.includes('@popperjs')) { + return 'vendor-popper' + } + + return 'vendor-misc' + } + }, onwarn(warning, warn) { // Suppress vite:reporter warnings about mixed static/dynamic imports // These are informational warnings about code splitting, not errors