Browse Source

improve image rendering, clean image tags, and utlitize the full imeta tags from NIP-92

imwald
Silberengel 5 months ago
parent
commit
290293be36
  1. 350
      package-lock.json
  2. 6
      package.json
  3. 32
      src/components/Content/index.tsx
  4. 32
      src/components/Image/index.tsx
  5. 5
      src/components/ImageGallery/index.tsx
  6. 15
      src/components/ImageWithLightbox/index.tsx
  7. 42
      src/components/Note/DiscussionContent/index.tsx
  8. 118
      src/components/UniversalContent/SimpleContent.tsx
  9. 2
      src/components/ui/skeleton.tsx
  10. 15
      src/lib/nostr-address.ts
  11. 115
      src/lib/tag.ts
  12. 5
      src/types/index.d.ts

350
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "jumble", "name": "jumble",
"version": "0.1.0", "version": "10.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jumble", "name": "jumble",
"version": "0.1.0", "version": "10.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
@ -42,6 +42,7 @@
"@tiptap/starter-kit": "^2.12.0", "@tiptap/starter-kit": "^2.12.0",
"@tiptap/suggestion": "^2.12.0", "@tiptap/suggestion": "^2.12.0",
"@webbtc/webln-types": "^3.0.0", "@webbtc/webln-types": "^3.0.0",
"asciidoctor": "^2.2.8",
"blossom-client-sdk": "^4.1.0", "blossom-client-sdk": "^4.1.0",
"blurhash": "^2.0.5", "blurhash": "^2.0.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -55,8 +56,10 @@
"emoji-picker-react": "^4.12.2", "emoji-picker-react": "^4.12.2",
"flexsearch": "^0.7.43", "flexsearch": "^0.7.43",
"franc-min": "^6.2.0", "franc-min": "^6.2.0",
"highlight.js": "^11.9.0",
"i18next": "^24.2.0", "i18next": "^24.2.0",
"i18next-browser-languagedetector": "^8.0.4", "i18next-browser-languagedetector": "^8.0.4",
"katex": "^0.16.9",
"lru-cache": "^11.0.2", "lru-cache": "^11.0.2",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
@ -68,6 +71,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^15.2.0", "react-i18next": "^15.2.0",
"react-katex": "^3.0.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-simple-pull-to-refresh": "^1.3.3", "react-simple-pull-to-refresh": "^1.3.3",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
@ -123,6 +127,41 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@asciidoctor/cli": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@asciidoctor/cli/-/cli-3.5.0.tgz",
"integrity": "sha512-/VMHXcZBnZ9vgWfmqk9Hu0x0gMjPLup0YGq/xA8qCQuk11kUIZNMVQwgSsIUzOEwJqIUD7CgncJdtfwv1Ndxuw==",
"license": "MIT",
"dependencies": {
"yargs": "16.2.0"
},
"bin": {
"asciidoctor": "bin/asciidoctor",
"asciidoctorjs": "bin/asciidoctor"
},
"engines": {
"node": ">=8.11",
"npm": ">=5.0.0"
},
"peerDependencies": {
"@asciidoctor/core": "^2.0.0-rc.1"
}
},
"node_modules/@asciidoctor/core": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.8.tgz",
"integrity": "sha512-oozXk7ZO1RAd/KLFLkKOhqTcG4GO3CV44WwOFg2gMcCsqCUTarvMT7xERIoWW2WurKbB0/ce+98r01p8xPOlBw==",
"license": "MIT",
"dependencies": {
"asciidoctor-opal-runtime": "0.3.3",
"unxhr": "1.0.1"
},
"engines": {
"node": ">=8.11",
"npm": ">=5.0.0",
"yarn": ">=1.1.0"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.26.2", "version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@ -5805,6 +5844,56 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/asciidoctor": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/asciidoctor/-/asciidoctor-2.2.8.tgz",
"integrity": "sha512-G+sDYWnNo+QHRkIvN5k7ASbvrd2bHuNXHlZ83+PjVFYtl0//as5iebq+Bdf3aSwXrkM7akcEJPUpdTjjP0MgYw==",
"license": "MIT",
"dependencies": {
"@asciidoctor/cli": "3.5.0",
"@asciidoctor/core": "2.2.8"
},
"bin": {
"asciidoctor": "bin/asciidoctor",
"asciidoctorjs": "bin/asciidoctor"
},
"engines": {
"node": ">=8.11",
"npm": ">=5.0.0",
"yarn": ">=1.1.0"
}
},
"node_modules/asciidoctor-opal-runtime": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.3.tgz",
"integrity": "sha512-/CEVNiOia8E5BMO9FLooo+Kv18K4+4JBFRJp8vUy/N5dMRAg+fRNV4HA+o6aoSC79jVU/aT5XvUpxSxSsTS8FQ==",
"license": "MIT",
"dependencies": {
"glob": "7.1.3",
"unxhr": "1.0.1"
},
"engines": {
"node": ">=8.11"
}
},
"node_modules/asciidoctor-opal-runtime/node_modules/glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
}
},
"node_modules/async": { "node_modules/async": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@ -5978,7 +6067,6 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -6247,6 +6335,75 @@
"url": "https://polar.sh/cva" "url": "https://polar.sh/cva"
} }
}, },
"node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -6721,8 +6878,7 @@
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
"dev": true
}, },
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
@ -7278,7 +7434,6 @@
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -7704,8 +7859,7 @@
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
"dev": true
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
@ -7766,6 +7920,15 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
@ -8049,6 +8212,15 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/html-parse-stringify": { "node_modules/html-parse-stringify": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@ -8169,7 +8341,6 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": { "dependencies": {
"once": "^1.3.0", "once": "^1.3.0",
"wrappy": "1" "wrappy": "1"
@ -8178,8 +8349,7 @@
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"dev": true
}, },
"node_modules/inline-style-parser": { "node_modules/inline-style-parser": {
"version": "0.2.4", "version": "0.2.4",
@ -8791,6 +8961,31 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/katex": {
"version": "0.16.25",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.25.tgz",
"integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/katex/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -9803,7 +9998,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -10006,7 +10200,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
} }
@ -10116,7 +10309,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -10375,6 +10567,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/property-information": { "node_modules/property-information": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@ -10675,6 +10879,26 @@
} }
} }
}, },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT",
"peer": true
},
"node_modules/react-katex": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-katex/-/react-katex-3.1.0.tgz",
"integrity": "sha512-At9uLOkC75gwn2N+ZXc5HD8TlATsB+3Hkp9OGs6uA8tM3dwZ3Wljn74Bk3JyHFPgSnesY/EMrIAB1WJwqZqejA==",
"license": "MIT",
"dependencies": {
"katex": "^0.16.0"
},
"peerDependencies": {
"prop-types": "^15.8.1",
"react": ">=15.3.2 <20"
}
},
"node_modules/react-markdown": { "node_modules/react-markdown": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
@ -10985,6 +11209,15 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": { "node_modules/require-from-string": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -12210,6 +12443,15 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/unxhr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.0.1.tgz",
"integrity": "sha512-MAhukhVHyaLGDjyDYhy8gVjWJyhTECCdNsLwlMoGFoNJ3o79fpQhtQuzmAE4IxCMDwraF4cW8ZjpAV0m9CRQbg==",
"license": "MIT",
"engines": {
"node": ">=8.11"
}
},
"node_modules/upath": { "node_modules/upath": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
@ -13025,8 +13267,16 @@
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"dev": true },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
}, },
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
@ -13045,6 +13295,74 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yet-another-react-lightbox": { "node_modules/yet-another-react-lightbox": {
"version": "3.21.7", "version": "3.21.7",
"resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.21.7.tgz", "resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.21.7.tgz",

6
package.json

@ -87,7 +87,11 @@
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"yet-another-react-lightbox": "^3.21.7", "yet-another-react-lightbox": "^3.21.7",
"zod": "^3.24.1" "zod": "^3.24.1",
"asciidoctor": "^2.2.8",
"katex": "^0.16.9",
"react-katex": "^3.0.1",
"highlight.js": "^11.9.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",

32
src/components/Content/index.tsx

@ -12,6 +12,7 @@ import {
import { getImetaInfosFromEvent } from '@/lib/event' import { getImetaInfosFromEvent } from '@/lib/event'
import { getEmojiInfosFromEmojiTags, getImetaInfoFromImetaTag } from '@/lib/tag' import { getEmojiInfosFromEmojiTags, getImetaInfoFromImetaTag } from '@/lib/tag'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { cleanUrl } from '@/lib/url'
import mediaUpload from '@/services/media-upload.service' import mediaUpload from '@/services/media-upload.service'
import { TImetaInfo } from '@/types' import { TImetaInfo } from '@/types'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
@ -60,20 +61,43 @@ const Content = memo(
const allImages = nodes const allImages = nodes
.map((node) => { .map((node) => {
if (node.type === 'image') { if (node.type === 'image') {
// Always ensure we have a valid image info object
const imageInfo = imetaInfos.find((image) => image.url === node.data) const imageInfo = imetaInfos.find((image) => image.url === node.data)
if (imageInfo) { if (imageInfo) {
return imageInfo return imageInfo
} }
// Try to get imeta from media upload service
const tag = mediaUpload.getImetaTagByUrl(node.data) const tag = mediaUpload.getImetaTagByUrl(node.data)
return tag if (tag) {
? getImetaInfoFromImetaTag(tag, event?.pubkey) const parsedImeta = getImetaInfoFromImetaTag(tag, event?.pubkey)
: { url: node.data, pubkey: event?.pubkey } if (parsedImeta) {
return parsedImeta
}
}
// Fallback: always create a basic image info object with cleaned URL
return { url: cleanUrl(node.data), pubkey: event?.pubkey }
} }
if (node.type === 'images') { if (node.type === 'images') {
const urls = Array.isArray(node.data) ? node.data : [node.data] const urls = Array.isArray(node.data) ? node.data : [node.data]
return urls.map((url) => { return urls.map((url) => {
const imageInfo = imetaInfos.find((image) => image.url === url) const imageInfo = imetaInfos.find((image) => image.url === url)
return imageInfo ?? { url, pubkey: event?.pubkey } if (imageInfo) {
return imageInfo
}
// Try to get imeta from media upload service
const tag = mediaUpload.getImetaTagByUrl(url)
if (tag) {
const parsedImeta = getImetaInfoFromImetaTag(tag, event?.pubkey)
if (parsedImeta) {
return parsedImeta
}
}
// Fallback: always create a basic image info object with cleaned URL
return { url: cleanUrl(url), pubkey: event?.pubkey }
}) })
} }
return null return null

32
src/components/Image/index.tsx

@ -8,7 +8,7 @@ import { ImageOff } from 'lucide-react'
import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react' import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react'
export default function Image({ export default function Image({
image: { url, blurHash, pubkey, dim }, image: { url, blurHash, pubkey, dim, alt: imetaAlt, fallback },
alt, alt,
className = '', className = '',
classNames = {}, classNames = {},
@ -30,6 +30,10 @@ export default function Image({
const [hasError, setHasError] = useState(false) const [hasError, setHasError] = useState(false)
const [imageUrl, setImageUrl] = useState(url) const [imageUrl, setImageUrl] = useState(url)
const [tried, setTried] = useState(new Set()) const [tried, setTried] = useState(new Set())
const [fallbackIndex, setFallbackIndex] = useState(0)
// Use imeta alt text if available, otherwise use the passed alt prop
const finalAlt = imetaAlt || alt
useEffect(() => { useEffect(() => {
setImageUrl(url) setImageUrl(url)
@ -37,11 +41,21 @@ export default function Image({
setHasError(false) setHasError(false)
setDisplaySkeleton(true) setDisplaySkeleton(true)
setTried(new Set()) setTried(new Set())
setFallbackIndex(0)
}, [url]) }, [url])
if (hideIfError && hasError) return null if (hideIfError && hasError) return null
const handleError = async () => { const handleError = async () => {
// First, try fallback URLs from imeta if available
if (fallback && fallbackIndex < fallback.length) {
const nextFallbackUrl = fallback[fallbackIndex]
setFallbackIndex(prev => prev + 1)
setImageUrl(nextFallbackUrl)
return
}
// If no more fallbacks, try Blossom servers
let oldImageUrl: URL | undefined let oldImageUrl: URL | undefined
let hash: string | null = null let hash: string | null = null
try { try {
@ -88,9 +102,9 @@ export default function Image({
} }
return ( return (
<div className={cn('relative overflow-hidden', classNames.wrapper)} {...props}> <span className={cn('relative overflow-hidden inline-block', classNames.wrapper)} {...props}>
{displaySkeleton && ( {displaySkeleton && (
<div className="absolute inset-0 z-10"> <span className="absolute inset-0 z-10 inline-block">
{blurHash ? ( {blurHash ? (
<BlurHashCanvas <BlurHashCanvas
blurHash={blurHash} blurHash={blurHash}
@ -107,12 +121,12 @@ export default function Image({
)} )}
/> />
)} )}
</div> </span>
)} )}
{!hasError && ( {!hasError && (
<img <img
src={imageUrl} src={imageUrl}
alt={alt} alt={finalAlt}
decoding="async" decoding="async"
loading="lazy" loading="lazy"
onLoad={handleLoad} onLoad={handleLoad}
@ -127,17 +141,17 @@ export default function Image({
/> />
)} )}
{hasError && ( {hasError && (
<div <span
className={cn( className={cn(
'object-cover flex flex-col items-center justify-center w-full h-full bg-muted', 'object-cover flex flex-col items-center justify-center w-full h-full bg-muted inline-block',
className, className,
classNames.errorPlaceholder classNames.errorPlaceholder
)} )}
> >
{errorPlaceholder} {errorPlaceholder}
</div> </span>
)} )}
</div> </span>
) )
} }

5
src/components/ImageGallery/index.tsx

@ -106,7 +106,10 @@ export default function ImageGallery({
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<Lightbox <Lightbox
index={index} index={index}
slides={images.map(({ url }) => ({ src: url }))} slides={images.map(({ url, alt }) => ({
src: url,
alt: alt || url
}))}
plugins={[Zoom]} plugins={[Zoom]}
open={index >= 0} open={index >= 0}
close={() => setIndex(-1)} close={() => setIndex(-1)}

15
src/components/ImageWithLightbox/index.tsx

@ -38,15 +38,15 @@ export default function ImageWithLightbox({
if (!display) { if (!display) {
return ( return (
<div <span
className="text-primary hover:underline truncate w-fit cursor-pointer" className="text-primary hover:underline truncate w-fit cursor-pointer inline-block"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
setDisplay(true) setDisplay(true)
}} }}
> >
[{t('Click to load image')}] [{t('Click to load image')}]
</div> </span>
) )
} }
@ -57,7 +57,7 @@ export default function ImageWithLightbox({
} }
return ( return (
<div> <span className="inline-block">
<Image <Image
key={0} key={0}
className={className} className={className}
@ -73,7 +73,10 @@ export default function ImageWithLightbox({
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<Lightbox <Lightbox
index={index} index={index}
slides={[{ src: image.url }]} slides={[{
src: image.url,
alt: image.alt || image.url
}]}
plugins={[Zoom]} plugins={[Zoom]}
open={index >= 0} open={index >= 0}
close={() => setIndex(-1)} close={() => setIndex(-1)}
@ -89,6 +92,6 @@ export default function ImageWithLightbox({
</div>, </div>,
document.body document.body
)} )}
</div> </span>
) )
} }

42
src/components/Note/DiscussionContent/index.tsx

@ -1,10 +1,5 @@
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import SimpleContent from '../../UniversalContent/SimpleContent'
import NostrNode from '../LongFormArticle/NostrNode'
import { remarkNostr } from '../LongFormArticle/remarkNostr'
import { Components } from '../LongFormArticle/types'
export default function DiscussionContent({ export default function DiscussionContent({
event, event,
@ -13,37 +8,10 @@ export default function DiscussionContent({
event: Event event: Event
className?: string className?: string
}) { }) {
const components = useMemo(
() =>
({
nostr: (props) => (
<div className="not-prose my-2">
<NostrNode
rawText={props.rawText}
bech32Id={props.bech32Id}
/>
</div>
)
}) as Components,
[]
)
return ( return (
<div <SimpleContent
className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`} event={event}
> className={className}
<Markdown />
remarkPlugins={[remarkGfm, remarkNostr]}
urlTransform={(url) => {
if (url.startsWith('nostr:')) {
return url.slice(6) // Remove 'nostr:' prefix for rendering
}
return url
}}
components={components}
>
{event.content}
</Markdown>
</div>
) )
} }

118
src/components/UniversalContent/SimpleContent.tsx

@ -0,0 +1,118 @@
import { useMemo } from 'react'
import { cn } from '@/lib/utils'
import { cleanUrl } from '@/lib/url'
import { getImetaInfosFromEvent } from '@/lib/event'
import { Event } from 'nostr-tools'
import ImageWithLightbox from '../ImageWithLightbox'
interface SimpleContentProps {
event?: Event
content?: string
className?: string
}
export default function SimpleContent({
event,
content,
className
}: SimpleContentProps) {
const imetaInfos = useMemo(() => event ? getImetaInfosFromEvent(event) : [], [event])
const processedContent = useMemo(() => {
const rawContent = content || event?.content || ''
// Clean URLs
let cleaned = rawContent.replace(
/(https?:\/\/[^\s]+)/g,
(url) => {
try {
return cleanUrl(url)
} catch {
return url
}
}
)
return cleaned
}, [content, event?.content])
const renderContent = () => {
if (!processedContent) return null
// Split content by lines and process each line
const lines = processedContent.split('\n')
const elements: JSX.Element[] = []
let key = 0
lines.forEach((line) => {
// Check if line contains an image URL
const imageMatch = line.match(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|heic|svg))/i)
if (imageMatch) {
const imageUrl = imageMatch[1]
const imageInfo = imetaInfos.find((info) => info.url === imageUrl)
const imageData = imageInfo || { url: imageUrl, pubkey: event?.pubkey }
elements.push(
<div key={key++} className="my-4">
<ImageWithLightbox
image={imageData}
className="max-w-full h-auto rounded-lg cursor-zoom-in"
/>
</div>
)
// Add the rest of the line as text if there's anything else
const beforeImage = line.substring(0, imageMatch.index).trim()
const afterImage = line.substring(imageMatch.index! + imageUrl.length).trim()
if (beforeImage || afterImage) {
elements.push(
<div key={key++} className="mb-2">
{beforeImage && <span>{beforeImage}</span>}
{afterImage && <span>{afterImage}</span>}
</div>
)
}
} else {
// Regular text line
elements.push(
<div key={key++} className="mb-1">
{renderTextWithLinks(line)}
</div>
)
}
})
return elements
}
const renderTextWithLinks = (text: string) => {
// Simple link detection and rendering
const linkRegex = /(https?:\/\/[^\s]+)/g
const parts = text.split(linkRegex)
return parts.map((part, index) => {
if (linkRegex.test(part)) {
return (
<a
key={index}
href={part}
target="_blank"
rel="noreferrer noopener"
className="text-primary hover:underline break-words"
>
{part}
</a>
)
}
return <span key={index}>{part}</span>
})
}
return (
<div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
{renderContent()}
</div>
)
}

2
src/components/ui/skeleton.tsx

@ -1,7 +1,7 @@
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('animate-pulse rounded-md bg-primary/10', className)} {...props} /> return <span className={cn('animate-pulse rounded-md bg-primary/10 inline-block', className)} {...props} />
} }
export { Skeleton } export { Skeleton }

15
src/lib/nostr-address.ts

@ -29,6 +29,21 @@ export function prefixNostrAddresses(content: string): string {
return match return match
} }
// Check if the match is within a URL by looking for common URL patterns before it
// This includes http://, https://, and common URL characters like /, ?, #
const urlPattern = /(https?:\/\/|www\.|\/[^\/]*|\?[^=]*=|#[^\/]*\/)$/
if (urlPattern.test(beforeMatch)) {
return match
}
// Also check if the match is followed by URL-like characters
const afterMatch = content.substring(offset + match.length)
const urlSuffixPattern = /^(\/|\.|\?|#|&|%|\s|$)/
if (!urlSuffixPattern.test(afterMatch)) {
// If it's not followed by URL characters, it might be within a URL
return match
}
// Add nostr: prefix // Add nostr: prefix
return `nostr:${match}` return `nostr:${match}`
}) })

115
src/lib/tag.ts

@ -1,4 +1,5 @@
import { TEmoji, TImetaInfo } from '@/types' import { TEmoji, TImetaInfo } from '@/types'
import { cleanUrl } from './url'
import { isBlurhashValid } from 'blurhash' import { isBlurhashValid } from 'blurhash'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { isValidPubkey } from './pubkey' import { isValidPubkey } from './pubkey'
@ -48,11 +49,31 @@ export function generateBech32IdFromATag(tag: string[]) {
export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImetaInfo | null { export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImetaInfo | null {
if (tag[0] !== 'imeta') return null if (tag[0] !== 'imeta') return null
// Handle different imeta tag structures:
// Structure 1: ["imeta", "url https://example.com/image.jpg", "alt text", ...]
// Structure 2: ["imeta", "url", "https://example.com/image.jpg", "alt", "text", ...]
let url: string | undefined
// First try the space-separated format
const urlItem = tag.find((item) => item.startsWith('url ')) const urlItem = tag.find((item) => item.startsWith('url '))
const url = urlItem?.slice(4) if (urlItem) {
url = urlItem.slice(4)
} else {
// Try the separate element format
const urlIndex = tag.findIndex((item) => item === 'url')
if (urlIndex !== -1 && urlIndex + 1 < tag.length) {
url = tag[urlIndex + 1]
}
}
if (!url) return null if (!url) return null
const imeta: TImetaInfo = { url, pubkey } // Clean the URL to remove tracking parameters
const cleanedUrl = cleanUrl(url)
const imeta: TImetaInfo = { url: cleanedUrl, pubkey }
// Parse blurhash
const blurHashItem = tag.find((item) => item.startsWith('blurhash ')) const blurHashItem = tag.find((item) => item.startsWith('blurhash '))
const blurHash = blurHashItem?.slice(9) const blurHash = blurHashItem?.slice(9)
if (blurHash) { if (blurHash) {
@ -61,6 +82,8 @@ export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImeta
imeta.blurHash = blurHash imeta.blurHash = blurHash
} }
} }
// Parse dimensions
const dimItem = tag.find((item) => item.startsWith('dim ')) const dimItem = tag.find((item) => item.startsWith('dim '))
const dim = dimItem?.slice(4) const dim = dimItem?.slice(4)
if (dim) { if (dim) {
@ -69,6 +92,94 @@ export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImeta
imeta.dim = { width, height } imeta.dim = { width, height }
} }
} }
// Parse MIME type
let mimeType: string | undefined
// First try the space-separated format
const mItem = tag.find((item) => item.startsWith('m '))
if (mItem) {
mimeType = mItem.slice(2)
} else {
// Try the separate element format
const mIndex = tag.findIndex((item) => item === 'm')
if (mIndex !== -1 && mIndex + 1 < tag.length) {
mimeType = tag[mIndex + 1]
}
}
if (mimeType) {
imeta.m = mimeType
}
// Parse alt text
let altText: string | undefined
// First try the space-separated format
const altItem = tag.find((item) => item.startsWith('alt '))
if (altItem) {
altText = altItem.slice(4)
} else {
// Try the separate element format
const altIndex = tag.findIndex((item) => item === 'alt')
if (altIndex !== -1 && altIndex + 1 < tag.length) {
altText = tag[altIndex + 1]
}
}
if (altText) {
imeta.alt = altText
}
// Parse SHA256 hash
let hash: string | undefined
// First try the space-separated format
const xItem = tag.find((item) => item.startsWith('x '))
if (xItem) {
hash = xItem.slice(2)
} else {
// Try the separate element format
const xIndex = tag.findIndex((item) => item === 'x')
if (xIndex !== -1 && xIndex + 1 < tag.length) {
hash = tag[xIndex + 1]
}
}
if (hash) {
imeta.x = hash
}
// Parse fallback URLs
const fallbackUrls: string[] = []
// First try the space-separated format
const fallbackItems = tag.filter((item) => item.startsWith('fallback '))
fallbackItems.forEach((item) => {
const url = item.slice(9)
if (url) fallbackUrls.push(cleanUrl(url))
})
// Also try the separate element format
let fallbackIndex = 0
while (fallbackIndex < tag.length) {
const index = tag.findIndex((item, i) => i >= fallbackIndex && item === 'fallback')
if (index === -1 || index + 1 >= tag.length) break
const url = tag[index + 1]
if (url) {
const cleanedUrl = cleanUrl(url)
if (!fallbackUrls.includes(cleanedUrl)) {
fallbackUrls.push(cleanedUrl)
}
}
fallbackIndex = index + 1
}
if (fallbackUrls.length > 0) {
imeta.fallback = fallbackUrls
}
return imeta return imeta
} }

5
src/types/index.d.ts vendored

@ -117,6 +117,11 @@ export type TImetaInfo = {
blurHash?: string blurHash?: string
dim?: { width: number; height: number } dim?: { width: number; height: number }
pubkey?: string pubkey?: string
// NIP-92 fields
m?: string // MIME type
alt?: string // Alternative text
x?: string // SHA256 hash as specified in NIP 94
fallback?: string[] // Array of fallback URLs
} }
export type TPublishOptions = { export type TPublishOptions = {

Loading…
Cancel
Save