Compare commits
No commits in common. "staging" and "170-editor-update" have entirely different histories.
staging
...
170-editor
@ -1,10 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2022: true },
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
@ -12,7 +12,7 @@ module.exports = {
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true }
|
||||
]
|
||||
}
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
178
package-lock.json
generated
178
package-lock.json
generated
@ -10,8 +10,8 @@
|
||||
"dependencies": {
|
||||
"@getalby/lightning-tools": "5.0.3",
|
||||
"@mdxeditor/editor": "^3.20.0",
|
||||
"@nostr-dev-kit/ndk": "2.11.0",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||
"@reduxjs/toolkit": "2.2.6",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"axios": "^1.7.9",
|
||||
@ -32,13 +32,13 @@
|
||||
"react": "^18.3.1",
|
||||
"react-countdown": "2.3.5",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-redux": "9.1.2",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-toastify": "10.0.5",
|
||||
"react-window": "1.8.10",
|
||||
"swiper": "11.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"uuid": "10.0.0",
|
||||
"webln": "0.3.2"
|
||||
},
|
||||
@ -2057,6 +2057,12 @@
|
||||
"react-dom": ">= 18 || >= 19"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixmark-io/domino": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||
@ -2134,19 +2140,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.11.0.tgz",
|
||||
"integrity": "sha512-FKIMtcVsVcquzrC+yir9lOXHCIHmQ3IKEVCMohqEB7N96HjP2qrI9s5utbjI3lkavFNF5tXg1Gp9ODEo7XCfLA==",
|
||||
"license": "MIT",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.0.tgz",
|
||||
"integrity": "sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==",
|
||||
"dependencies": {
|
||||
"@noble/curves": "^1.6.0",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@noble/secp256k1": "^2.1.0",
|
||||
"@scure/base": "^1.1.9",
|
||||
"debug": "^4.3.6",
|
||||
"light-bolt11-decoder": "^3.2.0",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.3.1",
|
||||
"@noble/secp256k1": "^2.0.0",
|
||||
"@scure/base": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"node-fetch": "^3.3.1",
|
||||
"nostr-tools": "^2.7.1",
|
||||
"tseep": "^1.2.2",
|
||||
"tseep": "^1.1.1",
|
||||
"typescript-lru-cache": "^2.0.0",
|
||||
"utf8-buffer": "^1.0.0",
|
||||
"websocket-polyfill": "^0.0.3"
|
||||
@ -2156,14 +2162,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk-cache-dexie": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.9.tgz",
|
||||
"integrity": "sha512-SZ5FjON0QPekiC7oW9Hy3JQxG0Oxxtud9LBa1q/A49JV/Qppv1x37nFHxi0XLxEbDgFTNYbaN27Zjfp2NPem2g==",
|
||||
"license": "MIT",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz",
|
||||
"integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==",
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "2.11.0",
|
||||
"debug": "^4.3.7",
|
||||
"dexie": "^4.0.8",
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
"debug": "^4.3.4",
|
||||
"dexie": "^4.0.2",
|
||||
"nostr-tools": "^2.4.0",
|
||||
"typescript-lru-cache": "^2.0.0"
|
||||
}
|
||||
@ -3932,15 +3937,6 @@
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/attributes-parser": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/attributes-parser/-/attributes-parser-2.2.3.tgz",
|
||||
@ -4363,6 +4359,14 @@
|
||||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||
@ -5030,6 +5034,28 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@ -5047,18 +5073,6 @@
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
|
||||
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@ -5147,6 +5161,17 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@ -6783,6 +6808,41 @@
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
|
||||
@ -7283,23 +7343,6 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "14.3.5",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz",
|
||||
"integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.4",
|
||||
"file-selector": "^2.1.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-error-boundary": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
|
||||
@ -7955,6 +7998,15 @@
|
||||
"resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz",
|
||||
"integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw=="
|
||||
},
|
||||
"node_modules/turndown": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz",
|
||||
"integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mixmark-io/domino": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||
@ -8328,6 +8380,14 @@
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webln": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/webln/-/webln-0.3.2.tgz",
|
||||
|
@ -12,8 +12,8 @@
|
||||
"dependencies": {
|
||||
"@getalby/lightning-tools": "5.0.3",
|
||||
"@mdxeditor/editor": "^3.20.0",
|
||||
"@nostr-dev-kit/ndk": "2.11.0",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||
"@reduxjs/toolkit": "2.2.6",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"axios": "^1.7.9",
|
||||
@ -34,13 +34,13 @@
|
||||
"react": "^18.3.1",
|
||||
"react-countdown": "2.3.5",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-redux": "9.1.2",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-toastify": "10.0.5",
|
||||
"react-window": "1.8.10",
|
||||
"swiper": "11.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"uuid": "10.0.0",
|
||||
"webln": "0.3.2"
|
||||
},
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { routerWithNdkContext as routerWithState } from 'routes'
|
||||
import { useEffect } from 'react'
|
||||
import { routerWithNdkContext } from 'routes'
|
||||
import { useNDKContext } from 'hooks'
|
||||
import './styles/styles.css'
|
||||
|
||||
function App() {
|
||||
const ndkContext = useNDKContext()
|
||||
const router = useMemo(() => routerWithState(ndkContext), [ndkContext])
|
||||
|
||||
useEffect(() => {
|
||||
// Find the element with id 'root'
|
||||
@ -25,7 +24,7 @@ function App() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <RouterProvider router={router} />
|
||||
return <RouterProvider router={routerWithNdkContext(ndkContext)} />
|
||||
}
|
||||
|
||||
export default App
|
||||
|
@ -1,19 +1,27 @@
|
||||
[
|
||||
{ "name": "gameplay ", "sub": ["difficulty"] },
|
||||
{ "name": "input", "sub": ["key mapping", "macro"] },
|
||||
{
|
||||
"name": "visual",
|
||||
"sub": ["textures", "lighting", "character models", "environment models"]
|
||||
"name": "audio",
|
||||
"sub": [
|
||||
{ "name": "music", "sub": ["background", "ambient"] },
|
||||
{
|
||||
"name": "sound effects",
|
||||
"sub": ["footsteps", "weapons"]
|
||||
},
|
||||
"voice"
|
||||
]
|
||||
},
|
||||
{ "name": "audio", "sub": ["sfx", "music", "voice"] },
|
||||
{ "name": "user interface", "sub": ["hud", "menu"] },
|
||||
{
|
||||
"name": "quality of life",
|
||||
"sub": ["bug fixes", "performance", "accessibility"]
|
||||
"name": "graphical",
|
||||
"sub": [
|
||||
{
|
||||
"name": "textures",
|
||||
"sub": ["highres textures", "lowres textures"]
|
||||
},
|
||||
"models",
|
||||
"shaders"
|
||||
]
|
||||
},
|
||||
"total conversions",
|
||||
"translation",
|
||||
"multiplayer",
|
||||
"clothing",
|
||||
"mod manager"
|
||||
{ "name": "user interface", "sub": ["hud", "menus", "icons"] },
|
||||
{ "name": "gameplay", "sub": ["mechanics", "balance", "ai"] },
|
||||
"bugfixes"
|
||||
]
|
||||
|
@ -6,9 +6,4 @@ Yandere Simulator,,https://image.nostr.build/54ba56b752bb9d411cbdc1d249fa0cb74c6
|
||||
Genshin Impact,,https://image.nostr.build/999fccf93cf16a2e0dd8e6f00595b0ab3b5cc6beff9fe4a52f64f427cce9aedd.jpg
|
||||
Zenless Zone Zero,,https://image.nostr.build/4a9b9c2cbef619552d0c123f8794286f35710dc7ca1ca0010380a630883eb2ca.jpg
|
||||
Ananta,,https://image.nostr.build/09fb30fe2c22784079e4c0a848410e709ff359af09b3f96b651c7dc249a35cdd.jpg
|
||||
Bloodborne,,https://image.nostr.build/9d20c246b539e43f1bebcf602f996fa6eb45cf585f05cc19a1d9f86a53201485.jpg
|
||||
The Elder Scrolls: Skyblivion,,https://cdn.nostrcheck.me/3cea4806b1e1a9829d30d5cb8a78011d4271c6474eb31531ec91f28110fe3f40/87868c8134d0ed30e74b99750e292fc85ad708d0add24c7c9113b086cd0784c3.webp
|
||||
Stellar Blade,,https://image.nostr.build/4dd76ef8ff985d2afc8b3eba323f18de7165659c4e925b2f06ae8b130372d5ae.jpg
|
||||
Bayonetta 2,,https://image.nostr.build/916f0b1fd4938114867654d7625f8e817b7f710d7729c81c911e1011fa74afad.jpg
|
||||
Grand Theft Auto: Vice City,,https://image.nostr.build/6f364ebb635cc878b284a06e8131dcec5eb4b574eece7ab206df8a9af639ddf6.jpg
|
||||
Alan Wake 2,,https://image.nostr.build/9c185ddb6f3c3b1f56835be8d36200eda7de0f36888b02523f4d39ade235ffab.jpg
|
||||
Bloodborne,,https://image.nostr.build/9d20c246b539e43f1bebcf602f996fa6eb45cf585f05cc19a1d9f86a53201485.jpg
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,3 @@
|
||||
Game Name,16 by 9 image,Boxart image
|
||||
Marvel's Spider-Man 2,,https://image.nostr.build/b5ef5ef8bd99daab73148145b024a1e6177160fd287ce829f82ba46e821490b6.jpg
|
||||
S.T.A.L.K.E.R. 2: Heart of Chornobyl,,https://image.nostr.build/f5b61071bebcc8deccfd71362696fb708649b9c528bec1c6964262ded4157843.jpg
|
||||
FINAL FANTASY VII REBIRTH,,https://image.nostr.build/e16228ccfad19669f17f83517ac621142ad7129c82d0e5f346a3523643f98a28.jpg
|
||||
NINJA GAIDEN 2 Black,,https://image.nostr.build/867b5d4ae7580f138b9d7e3bc41c0184e59fc22a758d2dcd9e941f8adf6d6e6e.jpg
|
||||
Rise of the Ronin,,https://image.nostr.build/5eba5c6efed335e1e6c74e7af29790a9cc519dbccdd81122e84336848a7e7866.jpg
|
||||
NINJA GAIDEN 4,,https://image.nostr.build/2b49b1571ba90450f95a9eb306d2ef9f3ad632dc6282125cc1651d17da17a439.jpg
|
||||
Batman Arkham Asylum,,https://image.nostr.build/ba5c07be4747957380213ad86ab83b8a4cb6b8ef0123ebb9863318ed1de6e43e.jpg
|
||||
Kingdom Hearts,,https://image.nostr.build/883b71c52b5b498aac20218b52af471ba89afb5cbb7072dc403da7446ca04e39.jpg
|
||||
Kingdom Hearts II,,https://image.nostr.build/24b6002029e91e4ad99b56aca9f20b076feb594ae48b320ba9122254add6b57e.jpg
|
||||
S.T.A.L.K.E.R. 2: Heart of Chornobyl,,https://image.nostr.build/f5b61071bebcc8deccfd71362696fb708649b9c528bec1c6964262ded4157843.jpg
|
|
Binary file not shown.
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 314 KiB |
@ -1,113 +0,0 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { DownloadUrl } from '../types'
|
||||
|
||||
export const DownloadDetailsPopup = ({
|
||||
title,
|
||||
url,
|
||||
hash,
|
||||
signatureKey,
|
||||
malwareScanLink,
|
||||
modVersion,
|
||||
customNote,
|
||||
mediaUrl,
|
||||
handleClose
|
||||
}: DownloadUrl & {
|
||||
handleClose: () => void
|
||||
}) => {
|
||||
return createPortal(
|
||||
<div className='popUpMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='popUpMainCardWrapper'>
|
||||
<div className='popUpMainCard'>
|
||||
<div className='popUpMainCardTop'>
|
||||
<div className='popUpMainCardTopInfo'>
|
||||
<h3>{title || 'Authentication Details'}</h3>
|
||||
</div>
|
||||
<div className='popUpMainCardTopClose' onClick={handleClose}>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-96 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pUMCB_Zaps'>
|
||||
<div className='pUMCB_ZapsInside'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTable'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Download URL</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{url}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>SHA-256 hash</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{hash}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Signature from</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{signatureKey}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Scan</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{malwareScanLink}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Mod Version</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{modVersion}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Note</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{customNote}</p>
|
||||
</div>
|
||||
</div>
|
||||
{typeof mediaUrl !== 'undefined' && mediaUrl !== '' && (
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Media</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<img
|
||||
src={mediaUrl}
|
||||
className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol_Img'
|
||||
alt=''
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
@ -154,7 +154,7 @@ export const CategoryFilterPopup = ({
|
||||
Choose categories...
|
||||
</label>
|
||||
<p className='labelDescriptionMain'>
|
||||
Choose one or more pre-definied or custom categories to filter out mods with.
|
||||
This is description for an input and how to use search here
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
@ -173,7 +173,7 @@ export const CategoryFilterPopup = ({
|
||||
>
|
||||
Custom categories
|
||||
</label>
|
||||
<p className='labelDescriptionMain'>Here's where your custom categories appear (You can add them in the above field. Example > banana > seed)</p>
|
||||
<p className='labelDescriptionMain'>Maybe</p>
|
||||
</div>
|
||||
<div
|
||||
className='inputMain'
|
||||
@ -230,7 +230,7 @@ export const CategoryFilterPopup = ({
|
||||
>
|
||||
Categories
|
||||
</label>
|
||||
<p className='labelDescriptionMain'>Here's where you select any of the pre-defined categories</p>
|
||||
<p className='labelDescriptionMain'>Maybe</p>
|
||||
</div>
|
||||
<div
|
||||
className='inputMain'
|
||||
|
@ -48,14 +48,10 @@ export const ModFilter = React.memo(
|
||||
{/* moderation filter options */}
|
||||
<Dropdown label={filterOptions.moderated}>
|
||||
{Object.values(ModeratedFilter).map((item, index) => {
|
||||
const isAdmin =
|
||||
userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
|
||||
if (item === ModeratedFilter.Only_Blocked && !isAdmin) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (item === ModeratedFilter.Unmoderated_Fully) {
|
||||
const isAdmin =
|
||||
userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
|
||||
const isOwnProfile =
|
||||
author && userState.auth && userState.user?.pubkey === author
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { InputError } from './Error'
|
||||
import { ImageUpload } from './ImageUpload'
|
||||
import '../../styles/styles.css'
|
||||
import React from 'react'
|
||||
import '../styles/styles.css'
|
||||
|
||||
interface InputFieldProps {
|
||||
label: string | React.ReactElement
|
||||
@ -62,6 +60,21 @@ export const InputField = React.memo(
|
||||
}
|
||||
)
|
||||
|
||||
type InputErrorProps = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export const InputError = ({ message }: InputErrorProps) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div className='errorMain'>
|
||||
<div className='errorMainColor'></div>
|
||||
<p className='errorMainText'>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CheckboxFieldProps {
|
||||
label: string
|
||||
name: string
|
||||
@ -144,63 +157,3 @@ export const CheckboxFieldUncontrolled = ({
|
||||
<input type='checkbox' className='CheckboxMain' {...rest} />
|
||||
</div>
|
||||
)
|
||||
|
||||
interface InputFieldWithImageUploadProps {
|
||||
label: string | React.ReactElement
|
||||
description?: string
|
||||
placeholder: string
|
||||
name: string
|
||||
inputMode?: 'url'
|
||||
value: string
|
||||
error?: string
|
||||
onInputChange: (name: string, value: string) => void
|
||||
}
|
||||
|
||||
export const InputFieldWithImageUpload = React.memo(
|
||||
({
|
||||
label,
|
||||
description,
|
||||
placeholder,
|
||||
name,
|
||||
inputMode,
|
||||
value,
|
||||
error,
|
||||
onInputChange
|
||||
}: InputFieldWithImageUploadProps) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
onInputChange(name, e.currentTarget.value)
|
||||
},
|
||||
[name, onInputChange]
|
||||
)
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(values: string[]) => {
|
||||
onInputChange(name, values[0])
|
||||
},
|
||||
[name, onInputChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label className='form-label labelMain'>{label}</label>
|
||||
{typeof description !== 'undefined' && (
|
||||
<p className='labelDescriptionMain'>{description}</p>
|
||||
)}
|
||||
|
||||
<ImageUpload onChange={handleFileChange} />
|
||||
|
||||
<input
|
||||
type='text'
|
||||
className='inputMain'
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
inputMode={inputMode}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{error && <InputError message={error} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
@ -1,14 +0,0 @@
|
||||
type InputErrorProps = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export const InputError = ({ message }: InputErrorProps) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div className='errorMain'>
|
||||
<div className='errorMainColor'></div>
|
||||
<p className='errorMainText'>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
.spinner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
backdrop-filter: blur(1px);
|
||||
pointer-events: none;
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
MediaOption,
|
||||
MEDIA_OPTIONS,
|
||||
ImageController,
|
||||
MEDIA_DROPZONE_OPTIONS
|
||||
} from '../../controllers'
|
||||
import { errorFeedback } from '../../types'
|
||||
import { MediaInputPopover } from './MediaInputPopover'
|
||||
import { Spinner } from 'components/Spinner'
|
||||
import styles from './ImageUpload.module.scss'
|
||||
|
||||
export interface ImageUploadProps {
|
||||
multiple?: boolean | undefined
|
||||
onChange: (values: string[]) => void
|
||||
}
|
||||
export const ImageUpload = React.memo(
|
||||
({ multiple = false, onChange }: ImageUploadProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [mediaOption, setMediaOption] = useState<MediaOption>(
|
||||
MEDIA_OPTIONS[0]
|
||||
)
|
||||
const handleOptionChange = useCallback(
|
||||
(mo: MediaOption) => () => {
|
||||
setMediaOption(mo)
|
||||
},
|
||||
[]
|
||||
)
|
||||
const handleUpload = useCallback(
|
||||
async (acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length) {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const imageController = new ImageController(mediaOption)
|
||||
const urls: string[] = []
|
||||
for (let i = 0; i < acceptedFiles.length; i++) {
|
||||
const file = acceptedFiles[i]
|
||||
urls.push(await imageController.post(file))
|
||||
}
|
||||
onChange(urls)
|
||||
} catch (error) {
|
||||
errorFeedback(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[mediaOption, onChange]
|
||||
)
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
acceptedFiles,
|
||||
isFileDialogActive,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
fileRejections
|
||||
} = useDropzone({
|
||||
...MEDIA_DROPZONE_OPTIONS,
|
||||
onDrop: handleUpload,
|
||||
multiple: multiple
|
||||
})
|
||||
|
||||
const dropzoneLabel = useMemo(
|
||||
() =>
|
||||
isFileDialogActive
|
||||
? 'Select files in dialog'
|
||||
: isDragActive
|
||||
? isDragAccept
|
||||
? 'Drop the files here...'
|
||||
: isDragReject
|
||||
? 'Drop the files here (one more more unsupported types)...'
|
||||
: 'Drop the files here...'
|
||||
: 'Click or drag files here',
|
||||
[isDragAccept, isDragActive, isDragReject, isFileDialogActive]
|
||||
)
|
||||
|
||||
return (
|
||||
<div aria-label='upload featuredImageUrl' className='uploadBoxMain'>
|
||||
<MediaInputPopover
|
||||
acceptedFiles={acceptedFiles}
|
||||
fileRejections={fileRejections}
|
||||
/>
|
||||
<div className='uploadBoxMainInside' {...getRootProps()} tabIndex={-1}>
|
||||
<input id='featuredImageUrl-upload' {...getInputProps()} />
|
||||
<span>{dropzoneLabel}</span>
|
||||
<div
|
||||
className='FiltersMainElement'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Image Host: {mediaOption.name}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{MEDIA_OPTIONS.map((mo) => {
|
||||
return (
|
||||
<div
|
||||
key={mo.host}
|
||||
onClick={handleOptionChange(mo)}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
>
|
||||
{mo.name}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className={styles.spinner}>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
@ -1,14 +0,0 @@
|
||||
.accordion-button::after {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
|
||||
top: unset !important;
|
||||
bottom: unset !important;
|
||||
}
|
||||
.accordion-body > * {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.accordion-item + .accordion-item {
|
||||
margin-top: 10px;
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import { FileError } from 'react-dropzone'
|
||||
import styles from './MediaInputError.module.scss'
|
||||
|
||||
type MediaInputErrorProps = {
|
||||
rootId: string
|
||||
index: number
|
||||
message: string
|
||||
errors?: readonly FileError[] | undefined
|
||||
}
|
||||
|
||||
export const MediaInputError = ({
|
||||
rootId,
|
||||
index,
|
||||
message,
|
||||
errors
|
||||
}: MediaInputErrorProps) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div className={['accordion-item', styles['accordion-item']].join(' ')}>
|
||||
<h2 className='accordion-header' role='tab'>
|
||||
<button
|
||||
className={[
|
||||
'accordion-button collapsed',
|
||||
styles['accordion-button']
|
||||
].join(' ')}
|
||||
type='button'
|
||||
data-bs-toggle='collapse'
|
||||
data-bs-target={`#${rootId} .item-${index}`}
|
||||
aria-expanded='false'
|
||||
aria-controls={`${rootId} .item-${index}`}
|
||||
>
|
||||
<div className='errorMain'>
|
||||
<div className='errorMainColor'></div>
|
||||
<p className='errorMainText'>{message}</p>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
{errors && (
|
||||
<div
|
||||
className={`accordion-collapse collapse item-${index}`}
|
||||
role='tabpanel'
|
||||
data-bs-parent={`#${rootId}`}
|
||||
>
|
||||
<div
|
||||
className={['accordion-body', styles['accordion-body']].join(' ')}
|
||||
>
|
||||
{errors.map((e) => {
|
||||
return typeof e === 'string' ? (
|
||||
<div className='errorMain' key={e}>
|
||||
{e}
|
||||
</div>
|
||||
) : (
|
||||
<div className='errorMain' key={e.code}>
|
||||
{e.message}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
.popover {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 0 16px 0px rgb(0 0 0 / 15%);
|
||||
background: #232323;
|
||||
z-index: 2;
|
||||
}
|
||||
.content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 25px;
|
||||
> *:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
.trigger {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 25px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.mediaInputError {
|
||||
--bs-accordion-color: unset;
|
||||
--bs-accordion-bg: unset;
|
||||
--bs-accordion-transition: unset;
|
||||
--bs-accordion-border-color: unset;
|
||||
--bs-accordion-border-width: unset;
|
||||
--bs-accordion-border-radius: unset;
|
||||
--bs-accordion-inner-border-radius: unset;
|
||||
--bs-accordion-btn-padding-x: unset;
|
||||
--bs-accordion-btn-padding-y: unset;
|
||||
--bs-accordion-btn-color: unset;
|
||||
--bs-accordion-btn-bg: unset;
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='gray'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon-width: 1.25rem;
|
||||
--bs-accordion-btn-icon-transform: rotate(-180deg);
|
||||
--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='gray'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-focus-border-color: unset;
|
||||
--bs-accordion-btn-focus-box-shadow: unset;
|
||||
--bs-accordion-body-padding-x: unset;
|
||||
--bs-accordion-body-padding-y: unset;
|
||||
--bs-accordion-active-color: unset;
|
||||
--bs-accordion-active-bg: unset;
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
import * as Popover from '@radix-ui/react-popover'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useMemo } from 'react'
|
||||
import { FileRejection, FileWithPath } from 'react-dropzone'
|
||||
import { MediaInputError } from './MediaInputError'
|
||||
import { InputSuccess } from './Success'
|
||||
import styles from './MediaInputPopover.module.scss'
|
||||
|
||||
interface MediaInputPopoverProps {
|
||||
acceptedFiles: readonly FileWithPath[]
|
||||
fileRejections: readonly FileRejection[]
|
||||
}
|
||||
|
||||
export const MediaInputPopover = ({
|
||||
acceptedFiles,
|
||||
fileRejections
|
||||
}: MediaInputPopoverProps) => {
|
||||
const uuid = useMemo(() => uuidv4(), [])
|
||||
const acceptedFileItems = useMemo(
|
||||
() =>
|
||||
acceptedFiles.map((file) => (
|
||||
<InputSuccess
|
||||
key={file.path}
|
||||
message={`${file.path} - ${file.size} bytes`}
|
||||
/>
|
||||
)),
|
||||
[acceptedFiles]
|
||||
)
|
||||
const fileRejectionItems = useMemo(() => {
|
||||
const id = `errors-${uuid}`
|
||||
return (
|
||||
<div
|
||||
className={`accordion accordion-flush ${styles.mediaInputError}`}
|
||||
role='tablist'
|
||||
id={id}
|
||||
>
|
||||
{fileRejections.map(({ file, errors }, index) => (
|
||||
<MediaInputError
|
||||
rootId={id}
|
||||
index={index}
|
||||
key={file.path}
|
||||
message={`${file.path} - ${file.size} bytes`}
|
||||
errors={errors}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}, [fileRejections, uuid])
|
||||
|
||||
if (acceptedFiles.length === 0 && fileRejections.length === 0) return null
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<div className={styles.trigger}>
|
||||
{acceptedFiles.length > 0 ? (
|
||||
<svg
|
||||
width='1.5em'
|
||||
height='1.5em'
|
||||
fill='currentColor'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 576 512'
|
||||
>
|
||||
<path d='M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 38.6C310.1 219.5 256 287.4 256 368c0 59.1 29.1 111.3 73.7 143.3c-3.2 .5-6.4 .7-9.7 .7L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM288 368a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm211.3-43.3c-6.2-6.2-16.4-6.2-22.6 0L416 385.4l-28.7-28.7c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6l40 40c6.2 6.2 16.4 6.2 22.6 0l72-72c6.2-6.2 6.2-16.4 0-22.6z' />
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
width='1.5em'
|
||||
height='1.5em'
|
||||
fill='tomato'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 576 512'
|
||||
>
|
||||
<path d='M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 38.6C310.1 219.5 256 287.4 256 368c0 59.1 29.1 111.3 73.7 143.3c-3.2 .5-6.4 .7-9.7 .7L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zm48 96a144 144 0 1 1 0 288 144 144 0 1 1 0-288zm0 240a24 24 0 1 0 0-48 24 24 0 1 0 0 48zm0-192c-8.8 0-16 7.2-16 16l0 80c0 8.8 7.2 16 16 16s16-7.2 16-16l0-80c0-8.8-7.2-16-16-16z' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content className={styles.popover} sideOffset={5}>
|
||||
<div className='popUpMainCardTop'>
|
||||
<div className='popUpMainCardTopInfo'>
|
||||
<h3>Selected files</h3>
|
||||
</div>
|
||||
<Popover.Close asChild aria-label='Close'>
|
||||
<div className='popUpMainCardTopClose'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-96 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{acceptedFileItems}
|
||||
{fileRejectionItems}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
type InputSuccessProps = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export const InputSuccess = ({ message }: InputSuccessProps) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div className='successMain'>
|
||||
<div className='successMainColor'></div>
|
||||
<p className='successMainText'>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { Dots } from 'components/Spinner'
|
||||
import { ZapSplit } from 'components/Zap'
|
||||
import {
|
||||
useAppSelector,
|
||||
@ -9,7 +8,7 @@ import {
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Addressable } from 'types'
|
||||
import { abbreviateNumber, log, LogType } from 'utils'
|
||||
import { abbreviateNumber } from 'utils'
|
||||
|
||||
type ZapProps = {
|
||||
addressable: Addressable
|
||||
@ -17,25 +16,15 @@ type ZapProps = {
|
||||
|
||||
export const Zap = ({ addressable }: ZapProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isAvailable, setIsAvailable] = useState(false)
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const { getTotalZapAmount, findMetadata } = useNDKContext()
|
||||
const { getTotalZapAmount } = useNDKContext()
|
||||
|
||||
useBodyScrollDisable(isOpen)
|
||||
|
||||
useDidMount(() => {
|
||||
findMetadata(addressable.author)
|
||||
.then((res) => {
|
||||
setIsAvailable(typeof res?.lud16 !== 'undefined' && res.lud16 !== '')
|
||||
})
|
||||
.catch((err) => {
|
||||
log(true, LogType.Error, err.message || err)
|
||||
})
|
||||
|
||||
getTotalZapAmount(
|
||||
addressable.author,
|
||||
addressable.id,
|
||||
@ -49,14 +38,8 @@ export const Zap = ({ addressable }: ZapProps) => {
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
})
|
||||
|
||||
// Hide button if the author hasn't set lud16
|
||||
if (!isAvailable) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -64,7 +47,7 @@ export const Zap = ({ addressable }: ZapProps) => {
|
||||
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CBolt ${
|
||||
hasZapped ? 'IBMSMSMBSS_D_CBActive' : ''
|
||||
}`}
|
||||
onClick={isLoading ? undefined : () => setIsOpen(true)}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||
<svg
|
||||
@ -79,7 +62,7 @@ export const Zap = ({ addressable }: ZapProps) => {
|
||||
</svg>
|
||||
</div>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>
|
||||
{isLoading ? <Dots /> : abbreviateNumber(totalZappedAmount)}
|
||||
{abbreviateNumber(totalZappedAmount)}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { PropsWithChildren, useEffect, useMemo, useState } from 'react'
|
||||
import { useNavigation } from 'react-router-dom'
|
||||
import styles from '../../styles/loadingSpinner.module.scss'
|
||||
|
||||
@ -6,7 +5,9 @@ interface Props {
|
||||
desc: string
|
||||
}
|
||||
|
||||
export const LoadingSpinner = ({ desc }: Props) => {
|
||||
export const LoadingSpinner = (props: Props) => {
|
||||
const { desc } = props
|
||||
|
||||
return (
|
||||
<div className={styles.loadingSpinnerOverlay}>
|
||||
<div className={styles.loadingSpinnerContainer}>
|
||||
@ -27,51 +28,3 @@ export const RouterLoadingSpinner = () => {
|
||||
|
||||
return <LoadingSpinner desc={`${desc}...`} />
|
||||
}
|
||||
|
||||
interface TimerLoadingSpinner {
|
||||
timeoutMs?: number
|
||||
countdownMs?: number
|
||||
}
|
||||
|
||||
export const TimerLoadingSpinner = ({
|
||||
timeoutMs = 10000,
|
||||
countdownMs = 30000,
|
||||
children
|
||||
}: PropsWithChildren<TimerLoadingSpinner>) => {
|
||||
const [show, setShow] = useState(false)
|
||||
const [timer, setTimer] = useState(
|
||||
Math.floor((countdownMs - timeoutMs) / 1000)
|
||||
)
|
||||
const startTime = useMemo(() => Date.now(), [])
|
||||
|
||||
useEffect(() => {
|
||||
let interval: number
|
||||
const timeout = window.setTimeout(() => {
|
||||
setShow(true)
|
||||
interval = window.setInterval(() => {
|
||||
const time = Date.now() - startTime
|
||||
const diff = Math.max(0, countdownMs - time)
|
||||
setTimer(Math.floor(diff / 1000))
|
||||
}, 1000)
|
||||
}, timeoutMs)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [countdownMs, startTime, timeoutMs])
|
||||
|
||||
return (
|
||||
<div className={styles.loadingSpinnerOverlay}>
|
||||
<div className={styles.loadingSpinnerContainer}>
|
||||
<div className={styles.loadingSpinner}></div>
|
||||
{children}
|
||||
{show && (
|
||||
<>
|
||||
<div>You can try again in {timer}s...</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ import {
|
||||
codeBlockPlugin,
|
||||
CodeToggle,
|
||||
CreateLink,
|
||||
diffSourcePlugin,
|
||||
DiffSourceToggleWrapper,
|
||||
directivesPlugin,
|
||||
headingsPlugin,
|
||||
imagePlugin,
|
||||
@ -70,42 +68,34 @@ export const Editor = React.memo(
|
||||
() => [
|
||||
toolbarPlugin({
|
||||
toolbarContents: () => (
|
||||
<DiffSourceToggleWrapper
|
||||
children={
|
||||
<>
|
||||
<UndoRedo />
|
||||
<Separator />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<CodeToggle />
|
||||
<Separator />
|
||||
<StrikeThroughSupSubToggles />
|
||||
<Separator />
|
||||
<ListsToggle />
|
||||
<Separator />
|
||||
<BlockTypeSelect />
|
||||
<Separator />
|
||||
<>
|
||||
<UndoRedo />
|
||||
<Separator />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<CodeToggle />
|
||||
<Separator />
|
||||
<StrikeThroughSupSubToggles />
|
||||
<Separator />
|
||||
<ListsToggle />
|
||||
<Separator />
|
||||
<BlockTypeSelect />
|
||||
<Separator />
|
||||
|
||||
<CreateLink />
|
||||
<InsertImage />
|
||||
<YouTubeButton />
|
||||
<CreateLink />
|
||||
<InsertImage />
|
||||
<YouTubeButton />
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
<InsertTable />
|
||||
<InsertThematicBreak />
|
||||
<InsertTable />
|
||||
<InsertThematicBreak />
|
||||
|
||||
<Separator />
|
||||
<InsertCodeBlock />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Separator />
|
||||
<InsertCodeBlock />
|
||||
</>
|
||||
)
|
||||
}),
|
||||
headingsPlugin(),
|
||||
diffSourcePlugin({
|
||||
viewMode: 'rich-text',
|
||||
diffMarkdown: markdown
|
||||
}),
|
||||
quotePlugin(),
|
||||
imagePlugin({
|
||||
ImageDialog: ImageDialog
|
||||
@ -128,7 +118,6 @@ export const Editor = React.memo(
|
||||
codeBlockEditorDescriptors: [PlainTextCodeEditorDescriptor]
|
||||
})
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
|
||||
|
@ -9,21 +9,12 @@ export const youtubeDirective: DirectiveConfig = {
|
||||
//::youtube{#<VIDEO_ID>}
|
||||
let vid: string = ''
|
||||
if (token.attrs && token.meta.name === 'youtube') {
|
||||
if (token.attrs.id) {
|
||||
vid = token.attrs.id as string // Get the video `id` attribute (common id style)
|
||||
} else if (token.attrs.vid) {
|
||||
vid = token.attrs.vid as string // Check for the `vid` attribute (youtube directive attribute style)
|
||||
} else {
|
||||
// Fallback for id
|
||||
// In case that video starts with the number it will not be recongizned as an id
|
||||
// We have to manually fetch it
|
||||
for (const attr in token.attrs) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(token.attrs, attr) &&
|
||||
attr.startsWith('#')
|
||||
) {
|
||||
vid = attr.replace('#', '')
|
||||
}
|
||||
for (const attr in token.attrs) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(token.attrs, attr) &&
|
||||
attr.startsWith('#')
|
||||
) {
|
||||
vid = attr.replace('#', '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,29 +18,18 @@ import { useGames } from '../hooks'
|
||||
import '../styles/styles.css'
|
||||
import {
|
||||
DownloadUrl,
|
||||
FormErrors,
|
||||
ModFormState,
|
||||
ModPageLoaderResult,
|
||||
ModPermissions,
|
||||
MODPERMISSIONS_CONF,
|
||||
MODPERMISSIONS_DESC,
|
||||
SubmitModActionResult
|
||||
ModPageLoaderResult
|
||||
} from '../types'
|
||||
import {
|
||||
initializeFormState,
|
||||
log,
|
||||
LogType,
|
||||
MOD_DRAFT_CACHE_KEY
|
||||
} from '../utils'
|
||||
import { CheckboxField, InputField, InputFieldWithImageUpload } from './Inputs'
|
||||
import { initializeFormState } from '../utils'
|
||||
import { CheckboxField, InputError, InputField } from './Inputs'
|
||||
import { OriginalAuthor } from './OriginalAuthor'
|
||||
import { CategoryAutocomplete } from './CategoryAutocomplete'
|
||||
import { AlertPopup } from './AlertPopup'
|
||||
import { Editor, EditorRef } from './Markdown/Editor'
|
||||
import { MEDIA_OPTIONS } from 'controllers'
|
||||
import { InputError } from './Inputs/Error'
|
||||
import { ImageUpload } from './Inputs/ImageUpload'
|
||||
import { useLocalCache } from 'hooks/useLocalCache'
|
||||
import { toast } from 'react-toastify'
|
||||
import TurndownService from 'turndown'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
interface GameOption {
|
||||
value: string
|
||||
@ -50,46 +39,19 @@ interface GameOption {
|
||||
export const ModForm = () => {
|
||||
const data = useLoaderData() as ModPageLoaderResult
|
||||
const mod = data?.mod
|
||||
const actionData = useActionData() as SubmitModActionResult
|
||||
const formErrors = useMemo(
|
||||
() => (actionData?.type === 'validation' ? actionData.error : undefined),
|
||||
[actionData]
|
||||
)
|
||||
const formErrors = useActionData() as FormErrors
|
||||
const navigation = useNavigation()
|
||||
const submit = useSubmit()
|
||||
const games = useGames()
|
||||
const [gameOptions, setGameOptions] = useState<GameOption[]>([])
|
||||
|
||||
// Enable cache for the new mod
|
||||
const isEditing = typeof mod !== 'undefined'
|
||||
const [cache, setCache, clearCache] =
|
||||
useLocalCache<ModFormState>(MOD_DRAFT_CACHE_KEY)
|
||||
const [formState, setFormState] = useState<ModFormState>(
|
||||
isEditing ? initializeFormState(mod) : cache ? cache : initializeFormState()
|
||||
initializeFormState(mod)
|
||||
)
|
||||
|
||||
// Enable backwards compatibility with the mods that used html
|
||||
const body = useMemo(() => {
|
||||
// Replace the most problematic HTML tags (<br>)
|
||||
const fixed = formState.body.replaceAll(/<br>/g, '\r\n')
|
||||
return fixed
|
||||
}, [formState.body])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
const newCache = _.cloneDeep(formState)
|
||||
|
||||
// Remove aTag, dTag and published_at from cache
|
||||
// These are used for editing and try again timeout
|
||||
newCache.aTag = ''
|
||||
newCache.dTag = ''
|
||||
newCache.published_at = 0
|
||||
|
||||
setCache(newCache)
|
||||
}
|
||||
}, [formState, isEditing, setCache])
|
||||
|
||||
const editorRef = useRef<EditorRef>(null)
|
||||
const sanitized = DOMPurify.sanitize(formState.body)
|
||||
const turndown = new TurndownService()
|
||||
turndown.keep(['sup', 'sub'])
|
||||
const markdown = turndown.turndown(sanitized)
|
||||
|
||||
useEffect(() => {
|
||||
const options = games.map((game) => ({
|
||||
@ -117,13 +79,6 @@ export const ModForm = () => {
|
||||
[]
|
||||
)
|
||||
|
||||
const handleRadioChange = useCallback((name: string, value: boolean) => {
|
||||
setFormState((prevState) => ({
|
||||
...prevState,
|
||||
[name]: value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const addScreenshotUrl = useCallback(() => {
|
||||
setFormState((prevState) => ({
|
||||
...prevState,
|
||||
@ -191,81 +146,47 @@ export const ModForm = () => {
|
||||
},
|
||||
[]
|
||||
)
|
||||
const [showTryAgainPopup, setShowTryAgainPopup] = useState<boolean>(false)
|
||||
useEffect(() => {
|
||||
const isTimeout = actionData?.type === 'timeout'
|
||||
setShowTryAgainPopup(isTimeout)
|
||||
if (isTimeout) {
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
aTag: actionData.data.aTag,
|
||||
dTag: actionData.data.dTag,
|
||||
published_at: actionData.data.published_at
|
||||
}))
|
||||
}
|
||||
}, [actionData])
|
||||
const handleTryAgainConfirm = useCallback(
|
||||
(confirm: boolean) => {
|
||||
setShowTryAgainPopup(false)
|
||||
|
||||
// Cancel if not confirmed
|
||||
if (!confirm) return
|
||||
submit(JSON.stringify(formState), {
|
||||
method: isEditing ? 'put' : 'post',
|
||||
encType: 'application/json'
|
||||
})
|
||||
},
|
||||
[formState, isEditing, submit]
|
||||
)
|
||||
|
||||
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
|
||||
const handleReset = useCallback(() => {
|
||||
const handleReset = () => {
|
||||
setShowConfirmPopup(true)
|
||||
}, [])
|
||||
const handleResetConfirm = useCallback(
|
||||
(confirm: boolean) => {
|
||||
setShowConfirmPopup(false)
|
||||
}
|
||||
const handleResetConfirm = (confirm: boolean) => {
|
||||
setShowConfirmPopup(false)
|
||||
|
||||
// Cancel if not confirmed
|
||||
if (!confirm) return
|
||||
// Cancel if not confirmed
|
||||
if (!confirm) return
|
||||
|
||||
// Reset fields to the initial or original existing data
|
||||
const initialState = initializeFormState(mod)
|
||||
// Editing
|
||||
if (mod) {
|
||||
const initial = initializeFormState(mod)
|
||||
|
||||
// Reset editor
|
||||
editorRef.current?.setMarkdown(initialState.body)
|
||||
setFormState(initialState)
|
||||
editorRef.current?.setMarkdown(initial.body)
|
||||
|
||||
// Clear cache
|
||||
!isEditing && clearCache()
|
||||
},
|
||||
[clearCache, isEditing, mod]
|
||||
)
|
||||
|
||||
const handlePublish = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
submit(JSON.stringify(formState), {
|
||||
method: isEditing ? 'put' : 'post',
|
||||
encType: 'application/json'
|
||||
})
|
||||
},
|
||||
[formState, isEditing, submit]
|
||||
)
|
||||
|
||||
const extraBoxRef = useRef<HTMLDivElement>(null)
|
||||
const handleExtraBoxButtonClick = () => {
|
||||
if (extraBoxRef.current) {
|
||||
if (extraBoxRef.current.style.display === '') {
|
||||
extraBoxRef.current.style.display = 'none'
|
||||
} else {
|
||||
extraBoxRef.current.style.display = ''
|
||||
}
|
||||
// Reset fields to the original existing data
|
||||
setFormState(initial)
|
||||
return
|
||||
}
|
||||
|
||||
// New - set form state to the initial (clear form state)
|
||||
setFormState(initializeFormState())
|
||||
}
|
||||
const handlePublish = () => {
|
||||
submit(JSON.stringify(formState), {
|
||||
method: mod ? 'put' : 'post',
|
||||
encType: 'application/json'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form className='IBMSMSMBS_Write' onSubmit={handlePublish}>
|
||||
<form
|
||||
className='IBMSMSMBS_Write'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handlePublish()
|
||||
}}
|
||||
>
|
||||
<GameDropdown
|
||||
options={gameOptions}
|
||||
selected={formState?.game}
|
||||
@ -287,15 +208,11 @@ export const ModForm = () => {
|
||||
<div className='inputMain'>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
markdown={body}
|
||||
markdown={markdown}
|
||||
placeholder="Here's what this mod is all about"
|
||||
onChange={(md) => {
|
||||
handleInputChange('body', md)
|
||||
}}
|
||||
onError={(payload) => {
|
||||
toast.error('Markdown error. Fix manually in the source mode.')
|
||||
log(true, LogType.Error, payload.error)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{typeof formErrors?.body !== 'undefined' && (
|
||||
@ -309,15 +226,16 @@ export const ModForm = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InputFieldWithImageUpload
|
||||
<InputField
|
||||
label='Featured Image URL'
|
||||
description={`We recommend to upload images to ${MEDIA_OPTIONS[0].host}`}
|
||||
description='We recommend to upload images to https://nostr.build/'
|
||||
type='text'
|
||||
inputMode='url'
|
||||
placeholder='Image URL'
|
||||
name='featuredImageUrl'
|
||||
value={formState.featuredImageUrl}
|
||||
error={formErrors?.featuredImageUrl}
|
||||
onInputChange={handleInputChange}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<InputField
|
||||
label='Summary'
|
||||
@ -381,24 +299,8 @@ export const ModForm = () => {
|
||||
</button>
|
||||
</div>
|
||||
<p className='labelDescriptionMain'>
|
||||
We recommend to upload images to {MEDIA_OPTIONS[0].host}
|
||||
We recommend to upload images to https://nostr.build/
|
||||
</p>
|
||||
|
||||
<ImageUpload
|
||||
multiple={true}
|
||||
onChange={(values) => {
|
||||
setFormState((prevState) => ({
|
||||
...prevState,
|
||||
screenshotsUrls: Array.from(
|
||||
new Set([
|
||||
...prevState.screenshotsUrls.filter((url) => url),
|
||||
...values
|
||||
])
|
||||
)
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
|
||||
{formState.screenshotsUrls.map((url, index) => (
|
||||
<Fragment key={`screenShot-${index}`}>
|
||||
<ScreenshotUrlFields
|
||||
@ -455,37 +357,20 @@ export const ModForm = () => {
|
||||
</div>
|
||||
<p className='labelDescriptionMain'>
|
||||
You can upload your game mod to Github, as an example, and keep
|
||||
updating it there (another option is{' '}
|
||||
<a
|
||||
href='https://catbox.moe/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
catbox.moe
|
||||
</a>
|
||||
). Also, it's advisable that you hash your package as well with your
|
||||
nostr public key. Malware scan service suggestion:{' '}
|
||||
<a
|
||||
href='https://virustotal.com'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
https://virustotal.com
|
||||
</a>
|
||||
updating it there (another option is catbox.moe). Also, it's advisable
|
||||
that you hash your package as well with your nostr public key.
|
||||
</p>
|
||||
|
||||
{formState.downloadUrls.map((download, index) => (
|
||||
<Fragment key={`download-${index}`}>
|
||||
<DownloadUrlFields
|
||||
index={index}
|
||||
title={download.title}
|
||||
url={download.url}
|
||||
hash={download.hash}
|
||||
signatureKey={download.signatureKey}
|
||||
malwareScanLink={download.malwareScanLink}
|
||||
modVersion={download.modVersion}
|
||||
customNote={download.customNote}
|
||||
mediaUrl={download.mediaUrl}
|
||||
onUrlChange={handleDownloadUrlChange}
|
||||
onRemove={removeDownloadUrl}
|
||||
/>
|
||||
@ -500,131 +385,6 @@ export const ModForm = () => {
|
||||
<InputError message={formErrors?.downloadUrls[0]} />
|
||||
)}
|
||||
</div>
|
||||
<div className='IBMSMSMBSSExtra'>
|
||||
<button
|
||||
className='btn btnMain IBMSMSMBSSExtraBtn'
|
||||
type='button'
|
||||
onClick={handleExtraBoxButtonClick}
|
||||
>
|
||||
Permissions & Details
|
||||
</button>
|
||||
<div
|
||||
className='IBMSMSMBSSExtraBox'
|
||||
ref={extraBoxRef}
|
||||
style={{
|
||||
display: 'none'
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className='labelDescriptionMain'
|
||||
style={{ marginBottom: `10px`, textAlign: `center` }}
|
||||
>
|
||||
What permissions users have with your published mod/post
|
||||
</p>
|
||||
<div className='IBMSMSMBSSExtraBoxElementWrapper'>
|
||||
{Object.keys(MODPERMISSIONS_CONF).map((k) => {
|
||||
const permKey = k as keyof ModPermissions
|
||||
const confKey = k as keyof typeof MODPERMISSIONS_CONF
|
||||
const modPermission = MODPERMISSIONS_CONF[confKey]
|
||||
const value = formState[permKey]
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSExtraBoxElement' key={k}>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
|
||||
<p>{modPermission.header}</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
|
||||
<label
|
||||
htmlFor={`${permKey}_true`}
|
||||
className='IBMSMSMBSSExtraBoxElementColChoice'
|
||||
>
|
||||
<p>
|
||||
{MODPERMISSIONS_DESC[`${permKey}_true`]}
|
||||
<br />
|
||||
</p>
|
||||
<input
|
||||
className='IBMSMSMBSSExtraBoxElementColChoiceRadio'
|
||||
type='radio'
|
||||
name={permKey}
|
||||
id={`${permKey}_true`}
|
||||
value={'true'}
|
||||
checked={
|
||||
typeof value !== 'undefined'
|
||||
? value === true
|
||||
: modPermission.default === true
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleRadioChange(
|
||||
permKey,
|
||||
e.currentTarget.value === 'true'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className='IBMSMSMBSSExtraBoxElementColChoiceBox'></div>
|
||||
</label>
|
||||
<label
|
||||
htmlFor={`${permKey}_false`}
|
||||
className='IBMSMSMBSSExtraBoxElementColChoice'
|
||||
>
|
||||
<p>
|
||||
{MODPERMISSIONS_DESC[`${permKey}_false`]}
|
||||
<br />
|
||||
</p>
|
||||
<input
|
||||
className='IBMSMSMBSSExtraBoxElementColChoiceRadio'
|
||||
type='radio'
|
||||
id={`${permKey}_false`}
|
||||
value={'false'}
|
||||
name={permKey}
|
||||
checked={
|
||||
typeof value !== 'undefined'
|
||||
? value === false
|
||||
: modPermission.default === false
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleRadioChange(
|
||||
permKey,
|
||||
e.currentTarget.value === 'true'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className='IBMSMSMBSSExtraBoxElementColChoiceBox'></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className='IBMSMSMBSSExtraBoxElement'>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
|
||||
<p>Publisher Notes</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
|
||||
<textarea
|
||||
className='inputMain'
|
||||
value={formState.publisherNotes || ''}
|
||||
onChange={(e) =>
|
||||
handleInputChange('publisherNotes', e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSExtraBoxElement'>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
|
||||
<p>Extra Credits</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
|
||||
<textarea
|
||||
className='inputMain'
|
||||
value={formState.extraCredits || ''}
|
||||
onChange={(e) =>
|
||||
handleInputChange('extraCredits', e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBS_WriteAction'>
|
||||
<button
|
||||
className='btn btnMain'
|
||||
@ -634,7 +394,7 @@ export const ModForm = () => {
|
||||
navigation.state === 'loading' || navigation.state === 'submitting'
|
||||
}
|
||||
>
|
||||
{isEditing ? 'Reset' : 'Clear fields'}
|
||||
{mod ? 'Reset' : 'Clear fields'}
|
||||
</button>
|
||||
<button
|
||||
className='btn btnMain'
|
||||
@ -646,21 +406,13 @@ export const ModForm = () => {
|
||||
{navigation.state === 'submitting' ? 'Publishing...' : 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
{showTryAgainPopup && (
|
||||
<AlertPopup
|
||||
handleConfirm={handleTryAgainConfirm}
|
||||
handleClose={() => setShowTryAgainPopup(false)}
|
||||
header={'Publish'}
|
||||
label={`Submission timed out. Do you want to try again?`}
|
||||
/>
|
||||
)}
|
||||
{showConfirmPopup && (
|
||||
<AlertPopup
|
||||
handleConfirm={handleResetConfirm}
|
||||
handleClose={() => setShowConfirmPopup(false)}
|
||||
header={'Are you sure?'}
|
||||
label={
|
||||
isEditing
|
||||
mod
|
||||
? `Are you sure you want to clear all changes?`
|
||||
: `Are you sure you want to clear all field data?`
|
||||
}
|
||||
@ -672,13 +424,11 @@ export const ModForm = () => {
|
||||
type DownloadUrlFieldsProps = {
|
||||
index: number
|
||||
url: string
|
||||
title?: string
|
||||
hash: string
|
||||
signatureKey: string
|
||||
malwareScanLink: string
|
||||
modVersion: string
|
||||
customNote: string
|
||||
mediaUrl?: string
|
||||
onUrlChange: (index: number, field: keyof DownloadUrl, value: string) => void
|
||||
onRemove: (index: number) => void
|
||||
}
|
||||
@ -687,13 +437,11 @@ const DownloadUrlFields = React.memo(
|
||||
({
|
||||
index,
|
||||
url,
|
||||
title,
|
||||
hash,
|
||||
signatureKey,
|
||||
malwareScanLink,
|
||||
modVersion,
|
||||
customNote,
|
||||
mediaUrl,
|
||||
onUrlChange,
|
||||
onRemove
|
||||
}: DownloadUrlFieldsProps) => {
|
||||
@ -731,28 +479,6 @@ const DownloadUrlFields = React.memo(
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className='inputWrapperMain'>
|
||||
<div className='inputWrapperMainBox'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-96 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M320 448c0 17.67-14.31 32-32 32H64c-17.69 0-32-14.33-32-32v-384C32 46.34 46.31 32.01 64 32.01S96 46.34 96 64.01v352h192C305.7 416 320 430.3 320 448z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
className='inputMain'
|
||||
name='title'
|
||||
placeholder='Download Title'
|
||||
value={title || ''}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className='inputWrapperMainBox'></div>
|
||||
</div>
|
||||
<div className='inputWrapperMain'>
|
||||
<div className='inputWrapperMainBox'>
|
||||
<svg
|
||||
@ -863,43 +589,6 @@ const DownloadUrlFields = React.memo(
|
||||
/>
|
||||
<div className='inputWrapperMainBox'></div>
|
||||
</div>
|
||||
<div className='inputWrapperMain'>
|
||||
<div className='inputWrapperMainBox'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-96 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M320 448c0 17.67-14.31 32-32 32H64c-17.69 0-32-14.33-32-32v-384C32 46.34 46.31 32.01 64 32.01S96 46.34 96 64.01v352h192C305.7 416 320 430.3 320 448z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
<ImageUpload
|
||||
onChange={(values) => {
|
||||
onUrlChange(index, 'mediaUrl', values[0])
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
className='inputMain'
|
||||
placeholder='Media URL'
|
||||
name='mediaUrl'
|
||||
value={mediaUrl || ''}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='inputWrapperMainBox'></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -924,7 +613,7 @@ const ScreenshotUrlFields = React.memo(
|
||||
type='text'
|
||||
className='inputMain'
|
||||
inputMode='url'
|
||||
placeholder='Image URL'
|
||||
placeholder='We recommend to upload images to https://nostr.build/'
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
@ -24,10 +24,7 @@ export const NsfwAlertPopup = ({
|
||||
<AlertPopup
|
||||
header='Confirm'
|
||||
label='Are you above 18 years of age?'
|
||||
handleClose={() => {
|
||||
handleConfirm(false)
|
||||
handleClose()
|
||||
}}
|
||||
handleClose={handleClose}
|
||||
handleConfirm={(confirm: boolean) => {
|
||||
setConfirmNsfw(confirm)
|
||||
handleConfirm(confirm)
|
||||
|
@ -1,22 +0,0 @@
|
||||
interface PostWarningsProps {
|
||||
type: 'user' | 'admin'
|
||||
}
|
||||
|
||||
export const PostWarnings = ({ type }: PostWarningsProps) => (
|
||||
<div className='IBMSMSMBSSWarning'>
|
||||
<p>
|
||||
{type === 'admin' ? (
|
||||
<>
|
||||
Warning: This post has been blocked/hidden by the site for one of the
|
||||
following reasons:
|
||||
<br />
|
||||
Malware, Not a Mod, Illegal, Spam, Verified Report of Unauthorized
|
||||
Repost.
|
||||
<br />
|
||||
</>
|
||||
) : (
|
||||
<>Notice: You have blocked this post</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
@ -155,7 +155,7 @@ export const Profile = ({ pubkey }: ProfileProps) => {
|
||||
<div className='IBMSMSMSSS_Author_TopWrapper'>
|
||||
<p className='IBMSMSMSSS_Author_Top_Name'>{displayName}</p>
|
||||
{/* Nip05 can sometimes be an empty object '{}' which causes the error */}
|
||||
{typeof nip05 === 'string' && nip05 !== '' && (
|
||||
{typeof nip05 === 'string' && (
|
||||
<p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p>
|
||||
)}
|
||||
</div>
|
||||
@ -191,7 +191,7 @@ export const Profile = ({ pubkey }: ProfileProps) => {
|
||||
{typeof nprofile !== 'undefined' && (
|
||||
<ProfileQRButtonWithPopUp nprofile={nprofile} />
|
||||
)}
|
||||
{typeof lud16 !== 'undefined' && lud16 !== '' && (
|
||||
{typeof lud16 !== 'undefined' && (
|
||||
<ZapButtonWithPopUp pubkey={pubkey} />
|
||||
)}
|
||||
</div>
|
||||
@ -383,12 +383,7 @@ const FollowButton = ({ pubkey }: FollowButtonProps) => {
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
return userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
return (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
return null
|
||||
}
|
||||
return (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { getRelayListForUser } from '@nostr-dev-kit/ndk'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import React, {
|
||||
Dispatch,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
@ -20,9 +19,6 @@ import {
|
||||
formatNumber,
|
||||
getTagValue,
|
||||
getZapAmount,
|
||||
log,
|
||||
LogType,
|
||||
timeout,
|
||||
unformatNumber
|
||||
} from '../utils'
|
||||
import { LoadingSpinner } from './LoadingSpinner'
|
||||
@ -128,7 +124,6 @@ type ZapQRProps = {
|
||||
handleQRExpiry: () => void
|
||||
setTotalZapAmount?: Dispatch<SetStateAction<number>>
|
||||
setHasZapped?: Dispatch<SetStateAction<boolean>>
|
||||
profileImage?: string
|
||||
}
|
||||
|
||||
export const ZapQR = React.memo(
|
||||
@ -137,10 +132,8 @@ export const ZapQR = React.memo(
|
||||
handleClose,
|
||||
handleQRExpiry,
|
||||
setTotalZapAmount,
|
||||
setHasZapped,
|
||||
profileImage,
|
||||
children
|
||||
}: PropsWithChildren<ZapQRProps>) => {
|
||||
setHasZapped
|
||||
}: ZapQRProps) => {
|
||||
const { ndk } = useNDKContext()
|
||||
|
||||
useDidMount(() => {
|
||||
@ -180,10 +173,7 @@ export const ZapQR = React.memo(
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='inputLabelWrapperMain inputLabelWrapperMainQR'
|
||||
style={{ alignItems: 'center' }}
|
||||
>
|
||||
<div className='inputLabelWrapperMain' style={{ alignItems: 'center' }}>
|
||||
<QRCodeSVG
|
||||
className='popUpMainCardBottomQR'
|
||||
onClick={onQrCodeClicked}
|
||||
@ -191,21 +181,6 @@ export const ZapQR = React.memo(
|
||||
height={235}
|
||||
width={235}
|
||||
/>
|
||||
{profileImage && (
|
||||
<div style={{ marginTop: '-20px' }}>
|
||||
<img
|
||||
src={profileImage}
|
||||
alt='Profile Avatar'
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '50px',
|
||||
borderRadius: '8px',
|
||||
border: 'solid 2px #494949',
|
||||
boxShadow: '0 0 4px 0 rgb(0, 0, 0, 0.1)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<label
|
||||
className='popUpMainCardBottomLnurl'
|
||||
onClick={() => {
|
||||
@ -217,7 +192,6 @@ export const ZapQR = React.memo(
|
||||
{paymentRequest.pr}
|
||||
</label>
|
||||
<Timer onTimerExpired={handleQRExpiry} />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -284,7 +258,7 @@ export const ZapPopUp = ({
|
||||
const [amount, setAmount] = useState<number>(0)
|
||||
const [message, setMessage] = useState('')
|
||||
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
|
||||
const [receiverMetadata, setRecieverMetadata] = useState<UserProfile>()
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@ -294,7 +268,7 @@ export const ZapPopUp = ({
|
||||
|
||||
const generatePaymentRequest =
|
||||
useCallback(async (): Promise<PaymentRequest | null> => {
|
||||
let userHexKey: string | undefined
|
||||
let userHexKey: string
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Getting user pubkey')
|
||||
@ -302,11 +276,7 @@ export const ZapPopUp = ({
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
userHexKey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!userHexKey) {
|
||||
@ -315,7 +285,7 @@ export const ZapPopUp = ({
|
||||
return null
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Finding receiver metadata')
|
||||
setLoadingSpinnerDesc('finding receiver metadata')
|
||||
|
||||
const receiverMetadata = await findMetadata(receiver)
|
||||
|
||||
@ -327,17 +297,12 @@ export const ZapPopUp = ({
|
||||
|
||||
if (!receiverMetadata?.pubkey) {
|
||||
setIsLoading(false)
|
||||
toast.error('Pubkey is missing in receiver metadata!')
|
||||
toast.error('pubkey is missing in receiver metadata!')
|
||||
return null
|
||||
}
|
||||
|
||||
setRecieverMetadata(receiverMetadata)
|
||||
|
||||
// Find the receiver's read relays.
|
||||
const receiverRelays = await Promise.race([
|
||||
getRelayListForUser(receiver, ndk),
|
||||
timeout(2000)
|
||||
])
|
||||
const receiverRelays = await getRelayListForUser(receiver, ndk)
|
||||
.then((ndkRelayList) => {
|
||||
if (ndkRelayList) return ndkRelayList.readRelayUrls
|
||||
return [] // Return an empty array if ndkRelayList is undefined
|
||||
@ -503,7 +468,6 @@ export const ZapPopUp = ({
|
||||
handleQRExpiry={handleQRExpiry}
|
||||
setTotalZapAmount={setTotalZapAmount}
|
||||
setHasZapped={setHasZapped}
|
||||
profileImage={receiverMetadata?.image}
|
||||
/>
|
||||
)}
|
||||
{lastNode}
|
||||
@ -584,7 +548,7 @@ export const ZapSplit = ({
|
||||
const generatePaymentInvoices = async () => {
|
||||
if (!amount) return null
|
||||
|
||||
let userHexKey: string | undefined
|
||||
let userHexKey: string
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Getting user pubkey')
|
||||
@ -592,11 +556,7 @@ export const ZapSplit = ({
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
userHexKey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!userHexKey) {
|
||||
@ -654,11 +614,7 @@ export const ZapSplit = ({
|
||||
|
||||
if (adminShare > 0 && admin?.pubkey && admin?.lud16) {
|
||||
// Find the receiver's read relays.
|
||||
// TODO: NDK should have native timeout in a future release
|
||||
const adminRelays = await Promise.race([
|
||||
getRelayListForUser(admin.pubkey as string, ndk),
|
||||
timeout(2000)
|
||||
])
|
||||
const adminRelays = await getRelayListForUser(admin.pubkey as string, ndk)
|
||||
.then((ndkRelayList) => {
|
||||
if (ndkRelayList) return ndkRelayList.readRelayUrls
|
||||
return [] // Return an empty array if ndkRelayList is undefined
|
||||
@ -759,8 +715,6 @@ export const ZapSplit = ({
|
||||
toast.warn('Webln is not present. Use QR code to send zap.')
|
||||
setInvoices(paymentInvoices)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const removeInvoice = (key: string) => {
|
||||
@ -775,56 +729,6 @@ export const ZapSplit = ({
|
||||
if (!invoices) return null
|
||||
|
||||
const authorInvoice = invoices.get('author')
|
||||
const feedback = (isFirst: boolean) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
flexWrap: 'wrap',
|
||||
gridGap: '10px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='btn btnMain'
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
cursor: 'default',
|
||||
background: isFirst ? undefined : 'unset'
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
1st Invoice
|
||||
</div>
|
||||
<div
|
||||
className='btn btnMain'
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
cursor: 'default',
|
||||
background: isFirst ? 'unset' : undefined
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
2nd Invoice
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
if (authorInvoice) {
|
||||
return (
|
||||
<ZapQR
|
||||
@ -834,10 +738,7 @@ export const ZapSplit = ({
|
||||
handleQRExpiry={() => removeInvoice('author')}
|
||||
setTotalZapAmount={setTotalZapAmount}
|
||||
setHasZapped={setHasZapped}
|
||||
profileImage={author?.image}
|
||||
>
|
||||
{feedback(true)}
|
||||
</ZapQR>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -852,10 +753,7 @@ export const ZapSplit = ({
|
||||
handleClose()
|
||||
}}
|
||||
handleQRExpiry={() => removeInvoice('admin')}
|
||||
profileImage={admin?.image}
|
||||
>
|
||||
{feedback(false)}
|
||||
</ZapQR>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,157 +0,0 @@
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { formatDate } from 'date-fns'
|
||||
import { useDidMount, useNDKContext } from 'hooks'
|
||||
import { useState } from 'react'
|
||||
import { useParams, useLocation, Link } from 'react-router-dom'
|
||||
import { getModPageRoute, getBlogPageRoute, getProfilePageRoute } from 'routes'
|
||||
import { CommentEvent, UserProfile } from 'types'
|
||||
import { hexToNpub } from 'utils'
|
||||
import { Reactions } from './Reactions'
|
||||
import { Zap } from './Zap'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { CommentContent } from './CommentContent'
|
||||
|
||||
interface CommentProps {
|
||||
comment: CommentEvent
|
||||
}
|
||||
export const Comment = ({ comment }: CommentProps) => {
|
||||
const { naddr } = useParams()
|
||||
const location = useLocation()
|
||||
const { ndk } = useNDKContext()
|
||||
const isMod = location.pathname.includes('/mod/')
|
||||
const isBlog = location.pathname.includes('/blog/')
|
||||
const baseUrl = naddr
|
||||
? isMod
|
||||
? getModPageRoute(naddr)
|
||||
: isBlog
|
||||
? getBlogPageRoute(naddr)
|
||||
: undefined
|
||||
: undefined
|
||||
const [commentEvents, setCommentEvents] = useState<CommentEvent[]>([])
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
|
||||
useDidMount(() => {
|
||||
comment.event.author.fetchProfile().then((res) => setProfile(res))
|
||||
ndk
|
||||
.fetchEvents({
|
||||
kinds: [NDKKind.Text, NDKKind.GenericReply],
|
||||
'#e': [comment.event.id]
|
||||
})
|
||||
.then((ndkEventsSet) => {
|
||||
setCommentEvents(
|
||||
Array.from(ndkEventsSet).map((ndkEvent) => ({
|
||||
event: ndkEvent
|
||||
}))
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: comment.event.pubkey
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCL_Comment'>
|
||||
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CommentTopPP'
|
||||
to={profileRoute}
|
||||
style={{
|
||||
background: `url('${
|
||||
profile?.image || ''
|
||||
}') center / cover no-repeat`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||
<Link className='IBMSMSMBSSCL_CTD_Name' to={profileRoute}>
|
||||
{profile?.displayName || profile?.name || ''}{' '}
|
||||
</Link>
|
||||
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
|
||||
{hexToNpub(comment.event.pubkey)}
|
||||
</Link>
|
||||
</div>
|
||||
{comment.event.created_at && (
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime'>
|
||||
{formatDate(comment.event.created_at * 1000, 'hh:mm aa')}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CADDate'>
|
||||
{formatDate(comment.event.created_at * 1000, 'dd/MM/yyyy')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||
{comment.status && (
|
||||
<p className='IBMSMSMBSSCL_CBTextStatus'>
|
||||
<span className='IBMSMSMBSSCL_CBTextStatusSpan'>Status:</span>
|
||||
{comment.status}
|
||||
</p>
|
||||
)}
|
||||
<CommentContent content={comment.event.content} />
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||
<Reactions {...comment.event.rawEvent()} />
|
||||
{/* <div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 -64 640 640'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>0</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div> */}
|
||||
{typeof profile?.lud16 !== 'undefined' && profile.lud16 !== '' && (
|
||||
<Zap {...comment.event.rawEvent()} />
|
||||
)}
|
||||
{comment.event.kind === NDKKind.GenericReply && (
|
||||
<>
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
|
||||
to={baseUrl + comment.event.encode()}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{commentEvents.length}
|
||||
</p>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
||||
</Link>
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
|
||||
to={baseUrl + comment.event.encode()}
|
||||
>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { useTextLimit } from 'hooks/useTextLimit'
|
||||
interface CommentContentProps {
|
||||
content: string
|
||||
}
|
||||
export const CommentContent = ({ content }: CommentContentProps) => {
|
||||
const { text, isTextOverflowing, isExpanded, toggle } = useTextLimit(content)
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='IBMSMSMBSSCL_CBText'>{text}</p>
|
||||
{isTextOverflowing && (
|
||||
<div className='IBMSMSMBSSCL_CBExpand' onClick={toggle}>
|
||||
<p>{isExpanded ? 'Hide' : 'View'} full post</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
type CommentFormProps = {
|
||||
handleSubmit: (content: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
export const CommentForm = ({ handleSubmit }: CommentFormProps) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [commentText, setCommentText] = useState('')
|
||||
|
||||
const handleComment = async () => {
|
||||
setIsSubmitting(true)
|
||||
const submitted = await handleSubmit(commentText)
|
||||
if (submitted) setCommentText('')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCommentsCreation'>
|
||||
<div className='IBMSMSMBSSCC_Top'>
|
||||
<textarea
|
||||
className='IBMSMSMBSSCC_Top_Box'
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCC_Bottom'>
|
||||
<button
|
||||
className='btnMain'
|
||||
onClick={handleComment}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Comment'}
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,335 +0,0 @@
|
||||
import { formatDate } from 'date-fns'
|
||||
import { useBodyScrollDisable, useNDKContext, useReplies } from 'hooks'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Link,
|
||||
useLoaderData,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useParams
|
||||
} from 'react-router-dom'
|
||||
import { getBlogPageRoute, getModPageRoute, getProfilePageRoute } from 'routes'
|
||||
import { CommentEvent, UserProfile } from 'types'
|
||||
import { CommentsLoaderResult } from 'types/comments'
|
||||
import { adjustTextareaHeight, handleCommentSubmit, hexToNpub } from 'utils'
|
||||
import { Reactions } from './Reactions'
|
||||
import { Zap } from './Zap'
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { Comment } from './Comment'
|
||||
import { useComments } from 'hooks/useComments'
|
||||
import { CommentContent } from './CommentContent'
|
||||
import { Dots } from 'components/Spinner'
|
||||
|
||||
export const CommentsPopup = () => {
|
||||
const { naddr } = useParams()
|
||||
const location = useLocation()
|
||||
const { ndk } = useNDKContext()
|
||||
useBodyScrollDisable(true)
|
||||
const isMod = location.pathname.includes('/mod/')
|
||||
const isBlog = location.pathname.includes('/blog/')
|
||||
const baseUrl = naddr
|
||||
? isMod
|
||||
? getModPageRoute(naddr)
|
||||
: isBlog
|
||||
? getBlogPageRoute(naddr)
|
||||
: undefined
|
||||
: undefined
|
||||
|
||||
const { event } = useLoaderData() as CommentsLoaderResult
|
||||
const {
|
||||
size,
|
||||
parent: replyEvent,
|
||||
isComplete,
|
||||
root: rootEvent
|
||||
} = useReplies(event.tagValue('e'))
|
||||
const isRoot = event.tagValue('a') === event.tagValue('A')
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
const { commentEvents, setCommentEvents } = useComments(
|
||||
event.author.pubkey,
|
||||
undefined,
|
||||
event.id
|
||||
)
|
||||
useEffect(() => {
|
||||
event.author.fetchProfile().then((res) => setProfile(res))
|
||||
}, [event.author])
|
||||
const profileRoute = useMemo(
|
||||
() =>
|
||||
getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: event.pubkey
|
||||
})
|
||||
),
|
||||
[event.pubkey]
|
||||
)
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [replyText, setReplyText] = useState('')
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.currentTarget.value
|
||||
setReplyText(value)
|
||||
adjustTextareaHeight(e.currentTarget)
|
||||
}, [])
|
||||
|
||||
const [visible, setVisible] = useState<CommentEvent[]>([])
|
||||
const discoveredCount = commentEvents.length - visible.length
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
useEffect(() => {
|
||||
// Initial loading to indicate comments fetching (stop after 5 seconds)
|
||||
const t = window.setTimeout(() => setIsLoading(false), 5000)
|
||||
return () => {
|
||||
window.clearTimeout(t)
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
setVisible(commentEvents)
|
||||
}
|
||||
}, [commentEvents, isLoading])
|
||||
const handleDiscoveredClick = () => {
|
||||
setVisible(commentEvents)
|
||||
}
|
||||
const handleSubmit = handleCommentSubmit(
|
||||
event,
|
||||
setCommentEvents,
|
||||
setVisible,
|
||||
ndk
|
||||
)
|
||||
|
||||
const handleComment = async () => {
|
||||
setIsSubmitting(true)
|
||||
const submitted = await handleSubmit(replyText)
|
||||
if (submitted) setReplyText('')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='popUpMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='popUpMainCardWrapper'>
|
||||
<div className='popUpMainCard'>
|
||||
<div className='popUpMainCardTop'>
|
||||
<div className='popUpMainCardTopInfo'>
|
||||
<h3>Comment replies</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='popUpMainCardTopClose'
|
||||
onClick={() => navigate('..')}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-96 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className='popUpMainCardBottom'>
|
||||
<div className='pUMCB_PrimeComment'>
|
||||
<div className='IBMSMSMBSSCL_Comment'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopOther'>
|
||||
<div className='IBMSMSMBSSCL_CTO'>
|
||||
{replyEvent && (
|
||||
<Link
|
||||
style={{
|
||||
...(!isComplete ? { pointerEvents: 'none' } : {})
|
||||
}}
|
||||
className='IBMSMSMBSSCL_CTOLink'
|
||||
to={baseUrl + replyEvent.encode()}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-32 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CTOLinkIcon'
|
||||
>
|
||||
<path d='M447.1 256C447.1 273.7 433.7 288 416 288H109.3l105.4 105.4c12.5 12.5 12.5 32.75 0 45.25C208.4 444.9 200.2 448 192 448s-16.38-3.125-22.62-9.375l-160-160c-12.5-12.5-12.5-32.75 0-45.25l160-160c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L109.3 224H416C433.7 224 447.1 238.3 447.1 256z'></path>
|
||||
</svg>
|
||||
</Link>
|
||||
)}
|
||||
<p className='IBMSMSMBSSCL_CTOText'>
|
||||
Reply Depth: <span>{size}</span>
|
||||
{!isComplete && <Dots />}
|
||||
</p>
|
||||
</div>
|
||||
{!isRoot && rootEvent && (
|
||||
<Link
|
||||
style={{
|
||||
...(!isComplete ? { pointerEvents: 'none' } : {})
|
||||
}}
|
||||
className='btn btnMain IBMSMSMBSSCL_CTOBtn'
|
||||
type='button'
|
||||
to={baseUrl + rootEvent.encode()}
|
||||
>
|
||||
Main Post {!isComplete && <Dots />}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CommentTopPP'
|
||||
to={profileRoute}
|
||||
style={{
|
||||
background: `url('${
|
||||
profile?.image || ''
|
||||
}') center / cover no-repeat`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CTD_Name'
|
||||
to={profileRoute}
|
||||
>
|
||||
{profile?.displayName || profile?.name || ''}{' '}
|
||||
</Link>
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CTD_Address'
|
||||
to={profileRoute}
|
||||
>
|
||||
{hexToNpub(event.pubkey)}
|
||||
</Link>
|
||||
</div>
|
||||
{event.created_at && (
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime'>
|
||||
{formatDate(event.created_at * 1000, 'hh:mm aa')}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CADDate'>
|
||||
{formatDate(event.created_at * 1000, 'dd/MM/yyyy')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||
<CommentContent content={event.content} />
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||
<Reactions {...event.rawEvent()} />
|
||||
|
||||
{/* <div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 -64 640 640'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>0</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{typeof profile?.lud16 !== 'undefined' &&
|
||||
profile.lud16 !== '' && <Zap {...event.rawEvent()} />}
|
||||
|
||||
{event.kind === NDKKind.GenericReply && (
|
||||
<>
|
||||
<span className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{commentEvents.length}
|
||||
</p>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
Replies
|
||||
</p>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pUMCB_CommentToPrime'>
|
||||
<div className='IBMSMSMBSSCC_Top'>
|
||||
<textarea
|
||||
className='IBMSMSMBSSCC_Top_Box postSocialTextarea'
|
||||
placeholder='Got something to say?'
|
||||
value={replyText}
|
||||
onChange={handleChange}
|
||||
style={{ height: '0px' }}
|
||||
></textarea>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCC_Bottom'>
|
||||
{/* <a className='IBMSMSMBSSCC_BottomButton'>Quote-Repost</a> */}
|
||||
<button
|
||||
onClick={handleComment}
|
||||
disabled={isSubmitting}
|
||||
className='IBMSMSMBSSCC_BottomButton'
|
||||
>
|
||||
{isSubmitting ? 'Replying...' : 'Reply'}
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{commentEvents.length > 0 && (
|
||||
<>
|
||||
<h3 className='IBMSMSMBSSCL_CommentNoteRepliesTitle'>
|
||||
Replies
|
||||
<button
|
||||
type='button'
|
||||
className='btnMain IBMSMSMBSSCL_CommentNoteRepliesTitleBtn'
|
||||
onClick={
|
||||
discoveredCount ? handleDiscoveredClick : undefined
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{isLoading ? (
|
||||
<>
|
||||
Discovering replies
|
||||
<Dots />
|
||||
</>
|
||||
) : discoveredCount ? (
|
||||
<>Load {discoveredCount} discovered replies</>
|
||||
) : (
|
||||
<>No new replies</>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</h3>
|
||||
<div className='pUMCB_RepliesToPrime'>
|
||||
{commentEvents.map((reply) => (
|
||||
<Comment key={reply.event.id} comment={reply} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import React, { Dispatch, SetStateAction } from 'react'
|
||||
import { AuthorFilterEnum, SortByEnum } from 'types'
|
||||
|
||||
export type FilterOptions = {
|
||||
sort: SortByEnum
|
||||
author: AuthorFilterEnum
|
||||
}
|
||||
|
||||
type FilterProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
export const Filter = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FilterProps) => {
|
||||
return (
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.author}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(AuthorFilterEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
author: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
@ -1,68 +0,0 @@
|
||||
import { NostrEvent } from '@nostr-dev-kit/ndk'
|
||||
import { Dots } from 'components/Spinner'
|
||||
import { useReactions } from 'hooks'
|
||||
|
||||
export const Reactions = (props: NostrEvent) => {
|
||||
const {
|
||||
isDataLoaded,
|
||||
likesCount,
|
||||
disLikesCount,
|
||||
handleReaction,
|
||||
hasReactedPositively,
|
||||
hasReactedNegatively
|
||||
} = useReactions({
|
||||
pubkey: props.pubkey,
|
||||
eTag: props.id!
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
|
||||
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
|
||||
}`}
|
||||
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? likesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
|
||||
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
|
||||
}`}
|
||||
onClick={isDataLoaded ? () => handleReaction() : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? disLikesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
import { NostrEvent } from '@nostr-dev-kit/ndk'
|
||||
import { ZapPopUp } from 'components/Zap'
|
||||
import {
|
||||
useAppSelector,
|
||||
useNDKContext,
|
||||
useBodyScrollDisable,
|
||||
useDidMount
|
||||
} from 'hooks'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { abbreviateNumber } from 'utils'
|
||||
|
||||
export const Zap = (props: NostrEvent) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const { getTotalZapAmount } = useNDKContext()
|
||||
|
||||
useBodyScrollDisable(isOpen)
|
||||
|
||||
useDidMount(() => {
|
||||
getTotalZapAmount(
|
||||
props.pubkey,
|
||||
props.id!,
|
||||
undefined,
|
||||
userState.user?.pubkey as string
|
||||
)
|
||||
.then((res) => {
|
||||
setTotalZappedAmount(res.accumulatedZapAmount)
|
||||
setHasZapped(res.hasZapped)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEBolt ${
|
||||
hasZapped ? 'IBMSMSMBSSCL_CAEBoltActive' : ''
|
||||
}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{abbreviateNumber(totalZappedAmount)}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ZapPopUp
|
||||
title='Tip/Zap'
|
||||
receiver={props.pubkey}
|
||||
eventId={props.id}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
setTotalZapAmount={setTotalZappedAmount}
|
||||
setHasZapped={setHasZapped}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,20 +1,48 @@
|
||||
import { Dots } from 'components/Spinner'
|
||||
import { useNDKContext } from 'hooks'
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { Dots, Spinner } from 'components/Spinner'
|
||||
import { ZapPopUp } from 'components/Zap'
|
||||
import { formatDate } from 'date-fns'
|
||||
import {
|
||||
useAppSelector,
|
||||
useBodyScrollDisable,
|
||||
useDidMount,
|
||||
useNDKContext,
|
||||
useReactions
|
||||
} from 'hooks'
|
||||
import { useComments } from 'hooks/useComments'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'
|
||||
import { useLoaderData } from 'react-router-dom'
|
||||
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { getProfilePageRoute } from 'routes'
|
||||
import {
|
||||
Addressable,
|
||||
AuthorFilterEnum,
|
||||
BlogPageLoaderResult,
|
||||
CommentEvent,
|
||||
ModPageLoaderResult,
|
||||
SortByEnum
|
||||
} from 'types'
|
||||
import { handleCommentSubmit } from 'utils'
|
||||
import { Filter, FilterOptions } from './Filter'
|
||||
import { CommentForm } from './CommentForm'
|
||||
import { Comment } from './Comment'
|
||||
CommentEventStatus,
|
||||
UserProfile
|
||||
} from 'types/index.ts'
|
||||
import { abbreviateNumber, hexToNpub, log, LogType, now } from 'utils'
|
||||
|
||||
enum SortByEnum {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest'
|
||||
}
|
||||
|
||||
enum AuthorFilterEnum {
|
||||
All_Comments = 'All Comments',
|
||||
Creator_Comments = 'Creator Comments'
|
||||
}
|
||||
|
||||
type FilterOptions = {
|
||||
sort: SortByEnum
|
||||
author: AuthorFilterEnum
|
||||
}
|
||||
|
||||
type Props = {
|
||||
addressable: Addressable
|
||||
@ -22,14 +50,11 @@ type Props = {
|
||||
}
|
||||
|
||||
export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
const { ndk } = useNDKContext()
|
||||
const { ndk, publish } = useNDKContext()
|
||||
const { commentEvents, setCommentEvents } = useComments(
|
||||
addressable.author,
|
||||
addressable.aTag
|
||||
)
|
||||
const { event } = useLoaderData() as
|
||||
| ModPageLoaderResult
|
||||
| BlogPageLoaderResult
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
sort: SortByEnum.Latest,
|
||||
author: AuthorFilterEnum.All_Comments
|
||||
@ -48,16 +73,122 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
setCommentCount(commentEvents.length)
|
||||
}, [commentEvents, setCommentCount])
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const handleSubmit = async (content: string): Promise<boolean> => {
|
||||
if (content === '') return false
|
||||
|
||||
let pubkey: string
|
||||
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
pubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
pubkey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!pubkey) {
|
||||
toast.error('Could not get user pubkey')
|
||||
return false
|
||||
}
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
content: content,
|
||||
pubkey: pubkey,
|
||||
kind: kinds.ShortTextNote,
|
||||
created_at: now(),
|
||||
tags: [
|
||||
['e', addressable.id],
|
||||
['a', addressable.aTag],
|
||||
['p', addressable.author]
|
||||
]
|
||||
}
|
||||
|
||||
const signedEvent = await window.nostr
|
||||
?.signEvent(unsignedEvent)
|
||||
.then((event) => event as Event)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to sign the event!')
|
||||
log(true, LogType.Error, 'Failed to sign the event!', err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!signedEvent) return false
|
||||
|
||||
setCommentEvents((prev) => [
|
||||
{
|
||||
...signedEvent,
|
||||
status: CommentEventStatus.Publishing
|
||||
},
|
||||
...prev
|
||||
])
|
||||
|
||||
const ndkEvent = new NDKEvent(ndk, signedEvent)
|
||||
publish(ndkEvent)
|
||||
.then((publishedOnRelays) => {
|
||||
if (publishedOnRelays.length === 0) {
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((event) => {
|
||||
if (event.id === signedEvent.id) {
|
||||
return {
|
||||
...event,
|
||||
status: CommentEventStatus.Failed
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
)
|
||||
} else {
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((event) => {
|
||||
if (event.id === signedEvent.id) {
|
||||
return {
|
||||
...event,
|
||||
status: CommentEventStatus.Published
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// when an event is successfully published remove the status from it after 15 seconds
|
||||
setTimeout(() => {
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((event) => {
|
||||
if (event.id === signedEvent.id) {
|
||||
delete event.status
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
)
|
||||
}, 15000)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('An error occurred in publishing comment', err)
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((event) => {
|
||||
if (event.id === signedEvent.id) {
|
||||
return {
|
||||
...event,
|
||||
status: CommentEventStatus.Failed
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const handleDiscoveredClick = () => {
|
||||
setVisible(commentEvents)
|
||||
}
|
||||
const [visible, setVisible] = useState<CommentEvent[]>([])
|
||||
const handleSubmit = handleCommentSubmit(
|
||||
event,
|
||||
setCommentEvents,
|
||||
setVisible,
|
||||
ndk
|
||||
)
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
setVisible(commentEvents)
|
||||
@ -68,22 +199,14 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
let filteredComments = visible
|
||||
if (filterOptions.author === AuthorFilterEnum.Creator_Comments) {
|
||||
filteredComments = filteredComments.filter(
|
||||
(comment) => comment.event.pubkey === addressable.author
|
||||
(comment) => comment.pubkey === addressable.author
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.sort === SortByEnum.Latest) {
|
||||
filteredComments.sort((a, b) =>
|
||||
a.event.created_at && b.event.created_at
|
||||
? b.event.created_at - a.event.created_at
|
||||
: 0
|
||||
)
|
||||
filteredComments.sort((a, b) => b.created_at - a.created_at)
|
||||
} else if (filterOptions.sort === SortByEnum.Oldest) {
|
||||
filteredComments.sort((a, b) =>
|
||||
a.event.created_at && b.event.created_at
|
||||
? a.event.created_at - b.event.created_at
|
||||
: 0
|
||||
)
|
||||
filteredComments.sort((a, b) => a.created_at - b.created_at)
|
||||
}
|
||||
|
||||
return filteredComments
|
||||
@ -97,35 +220,380 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
{/* Hide comment form if aTag is missing */}
|
||||
{!!addressable.aTag && <CommentForm handleSubmit={handleSubmit} />}
|
||||
<div>
|
||||
<button
|
||||
type='button'
|
||||
className='btnMain'
|
||||
onClick={discoveredCount ? handleDiscoveredClick : undefined}
|
||||
>
|
||||
<span>
|
||||
{isLoading ? (
|
||||
<>
|
||||
Discovering comments
|
||||
<Dots />
|
||||
</>
|
||||
) : discoveredCount ? (
|
||||
<>Load {discoveredCount} discovered comments</>
|
||||
) : (
|
||||
<>No new comments</>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<button
|
||||
type='button'
|
||||
className='btnMain'
|
||||
onClick={discoveredCount ? handleDiscoveredClick : undefined}
|
||||
>
|
||||
<span>Load {discoveredCount} discovered comments</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Filter
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
<div className='IBMSMSMBSSCommentsList'>
|
||||
{comments.map((comment) => (
|
||||
<Comment key={comment.event.id} comment={comment} />
|
||||
{comments.map((event) => (
|
||||
<Comment key={event.id} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type CommentFormProps = {
|
||||
handleSubmit: (content: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
const CommentForm = ({ handleSubmit }: CommentFormProps) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [commentText, setCommentText] = useState('')
|
||||
|
||||
const handleComment = async () => {
|
||||
setIsSubmitting(true)
|
||||
const submitted = await handleSubmit(commentText)
|
||||
if (submitted) setCommentText('')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCommentsCreation'>
|
||||
<div className='IBMSMSMBSSCC_Top'>
|
||||
<textarea
|
||||
className='IBMSMSMBSSCC_Top_Box'
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCC_Bottom'>
|
||||
<button
|
||||
className='btnMain'
|
||||
onClick={handleComment}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Comment'}
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type FilterProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
const Filter = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FilterProps) => {
|
||||
return (
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.author}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(AuthorFilterEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
author: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const Comment = (props: CommentEvent) => {
|
||||
const { findMetadata } = useNDKContext()
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
|
||||
useDidMount(() => {
|
||||
findMetadata(props.pubkey).then((res) => {
|
||||
setProfile(res)
|
||||
})
|
||||
})
|
||||
|
||||
const profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: props.pubkey
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCL_Comment'>
|
||||
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CommentTopPP'
|
||||
to={profileRoute}
|
||||
style={{
|
||||
background: `url('${
|
||||
profile?.image || ''
|
||||
}') center / cover no-repeat`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||
<Link className='IBMSMSMBSSCL_CTD_Name' to={profileRoute}>
|
||||
{profile?.displayName || profile?.name || ''}{' '}
|
||||
</Link>
|
||||
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
|
||||
{hexToNpub(props.pubkey)}
|
||||
</Link>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime'>
|
||||
{formatDate(props.created_at * 1000, 'hh:mm aa')}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CADDate'>
|
||||
{formatDate(props.created_at * 1000, 'dd/MM/yyyy')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||
{props.status && (
|
||||
<p className='IBMSMSMBSSCL_CBTextStatus'>
|
||||
<span className='IBMSMSMBSSCL_CBTextStatusSpan'>Status:</span>
|
||||
{props.status}
|
||||
</p>
|
||||
)}
|
||||
<p className='IBMSMSMBSSCL_CBText'>{props.content}</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||
<Reactions {...props} />
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 -64 640 640'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>0</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<Zap {...props} />
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>0</p>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
||||
</div>
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Reactions = (props: Event) => {
|
||||
const {
|
||||
isDataLoaded,
|
||||
likesCount,
|
||||
disLikesCount,
|
||||
handleReaction,
|
||||
hasReactedPositively,
|
||||
hasReactedNegatively
|
||||
} = useReactions({
|
||||
pubkey: props.pubkey,
|
||||
eTag: props.id
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
|
||||
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
|
||||
}`}
|
||||
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? likesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
|
||||
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
|
||||
}`}
|
||||
onClick={isDataLoaded ? () => handleReaction() : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? disLikesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Zap = (props: Event) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const { getTotalZapAmount } = useNDKContext()
|
||||
|
||||
useBodyScrollDisable(isOpen)
|
||||
|
||||
useDidMount(() => {
|
||||
getTotalZapAmount(
|
||||
props.pubkey,
|
||||
props.id,
|
||||
undefined,
|
||||
userState.user?.pubkey as string
|
||||
)
|
||||
.then((res) => {
|
||||
setTotalZappedAmount(res.accumulatedZapAmount)
|
||||
setHasZapped(res.hasZapped)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEBolt ${
|
||||
hasZapped ? 'IBMSMSMBSSCL_CAEBoltActive' : ''
|
||||
}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{abbreviateNumber(totalZappedAmount)}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ZapPopUp
|
||||
title='Tip/Zap'
|
||||
receiver={props.pubkey}
|
||||
eventId={props.id}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
setTotalZapAmount={setTotalZappedAmount}
|
||||
setHasZapped={setHasZapped}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -123,4 +123,3 @@ export const FALLBACK_PROFILE_IMAGE =
|
||||
'https://image.nostr.build/a305f4b43f74af3c6dcda42e6a798105a56ac1e3e7b74d7bef171896b3ba7520.png'
|
||||
|
||||
export const PROFILE_BLOG_FILTER_LIMIT = 20
|
||||
export const MAX_VISIBLE_TEXT_PER_COMMENT = 500
|
||||
|
@ -252,7 +252,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
log(
|
||||
false, // Too many failed requests, turned off for clarity
|
||||
true,
|
||||
LogType.Error,
|
||||
`An error occurred in fetching user's (${hexKey}) ${userRelaysType}`,
|
||||
err
|
||||
@ -369,14 +369,16 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const publish = async (event: NDKEvent): Promise<string[]> => {
|
||||
if (!event.sig) throw new Error('Before publishing first sign the event!')
|
||||
|
||||
try {
|
||||
const res = await event.publish(undefined, 10000)
|
||||
const relaysPublishedOn = Array.from(res)
|
||||
return relaysPublishedOn.map((relay) => relay.url)
|
||||
} catch (err) {
|
||||
console.error(`An error occurred in publishing event`, err)
|
||||
return []
|
||||
}
|
||||
return event
|
||||
.publish(undefined, 10000)
|
||||
.then((res) => {
|
||||
const relaysPublishedOn = Array.from(res)
|
||||
return relaysPublishedOn.map((relay) => relay.url)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`An error occurred in publishing event`, err)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,74 +0,0 @@
|
||||
import { DropzoneOptions } from 'react-dropzone'
|
||||
import { NostrCheckServer } from './nostrcheck-server'
|
||||
import { BaseError } from 'types'
|
||||
|
||||
export interface MediaOperations {
|
||||
post: (file: File) => Promise<string>
|
||||
}
|
||||
export type MediaStrategy = Omit<MediaOperations, 'auth'>
|
||||
|
||||
export interface MediaOption {
|
||||
name: string
|
||||
host: string
|
||||
type: 'nostrcheck-server' | 'route96'
|
||||
}
|
||||
|
||||
// nostr.build based dropzone options
|
||||
export const MEDIA_DROPZONE_OPTIONS: DropzoneOptions = {
|
||||
maxSize: 7000000,
|
||||
accept: {
|
||||
'image/*': ['.jpeg', '.png', '.jpg', '.gif', '.webp']
|
||||
}
|
||||
}
|
||||
|
||||
export const MEDIA_OPTIONS: MediaOption[] = [
|
||||
// {
|
||||
// name: 'nostr.build',
|
||||
// host: 'https://nostr.build/',
|
||||
// type: 'nostrcheck-server'
|
||||
// },
|
||||
{
|
||||
name: 'nostrcheck.me',
|
||||
host: 'https://nostrcheck.me/',
|
||||
type: 'nostrcheck-server'
|
||||
},
|
||||
{
|
||||
name: 'nostpic.com',
|
||||
host: 'https://nostpic.com/',
|
||||
type: 'nostrcheck-server'
|
||||
},
|
||||
{
|
||||
name: 'files.sovbit.host',
|
||||
host: 'https://files.sovbit.host/',
|
||||
type: 'nostrcheck-server'
|
||||
}
|
||||
// {
|
||||
// name: 'void.cat',
|
||||
// host: 'https://void.cat/',
|
||||
// type: 'route96'
|
||||
// }
|
||||
]
|
||||
|
||||
enum ImageErrorType {
|
||||
'TYPE_MISSING' = 'Media Option must include a type.'
|
||||
}
|
||||
|
||||
export class ImageController implements MediaStrategy {
|
||||
post: (file: File) => Promise<string>
|
||||
|
||||
constructor(mediaOption: MediaOption) {
|
||||
let strategy: MediaStrategy
|
||||
switch (mediaOption.type) {
|
||||
case 'nostrcheck-server':
|
||||
strategy = new NostrCheckServer(mediaOption.host)
|
||||
this.post = strategy.post
|
||||
break
|
||||
|
||||
case 'route96':
|
||||
throw new Error('Not implemented.')
|
||||
|
||||
default:
|
||||
throw new BaseError(ImageErrorType.TYPE_MISSING)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
import axios, { isAxiosError } from 'axios'
|
||||
import { NostrEvent, NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { type MediaOperations } from '.'
|
||||
import { store } from 'store'
|
||||
import { log, LogType, now } from 'utils'
|
||||
import { BaseError, handleError } from 'types'
|
||||
|
||||
// https://github.com/quentintaranpino/nostrcheck-server/blob/main/DOCS.md#media-post
|
||||
// Response object (other fields omitted for brevity)
|
||||
// {
|
||||
// "status": "success",
|
||||
// "nip94_event": {
|
||||
// "tags": [
|
||||
// [
|
||||
// "url",
|
||||
// "https://nostrcheck.me/media/62c76eb094369d938f5895442eef7f53ebbf019f69707d64e77d4d182b609309/c35277dbcedebb0e3b80361762c8baadb66dcdfb6396949e50630159a472c3b2.webp"
|
||||
// ],
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
|
||||
interface Response {
|
||||
status: 'success' | string
|
||||
nip94_event?: {
|
||||
tags?: string[][]
|
||||
}
|
||||
}
|
||||
|
||||
enum HandledErrorType {
|
||||
'PUBKEY' = 'Failed to get public key.',
|
||||
'SIGN' = 'Failed to sign the event.',
|
||||
'AXIOS_REQ' = 'Image upload failed. Try another host from the dropdown.',
|
||||
'AXIOS_RES' = 'Image upload failed. Reason: ',
|
||||
'AXIOS_ERR' = 'Image upload failed.',
|
||||
'NOSTR_CHECK_NO_SUCCESS' = 'Image upload was unsuccesfull.',
|
||||
'NOSTR_CHECK_BAD_EVENT' = 'Image upload failed. Please try again.'
|
||||
}
|
||||
|
||||
export class NostrCheckServer implements MediaOperations {
|
||||
#media = 'api/v2/media'
|
||||
#url: string
|
||||
|
||||
constructor(url: string) {
|
||||
this.#url = url[url.length - 1] === '/' ? url : `${url}/`
|
||||
}
|
||||
|
||||
post = async (file: File) => {
|
||||
const url = `${this.#url}${this.#media}`
|
||||
const auth = await this.auth()
|
||||
try {
|
||||
const response = await axios.postForm<Response>(
|
||||
url,
|
||||
{
|
||||
uploadType: 'media',
|
||||
file: file
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Nostr ' + auth,
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
responseType: 'json'
|
||||
}
|
||||
)
|
||||
|
||||
if (response.data.status !== 'success') {
|
||||
throw new BaseError(HandledErrorType.NOSTR_CHECK_NO_SUCCESS, {
|
||||
context: { ...response.data }
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
response.data &&
|
||||
response.data.nip94_event &&
|
||||
response.data.nip94_event.tags &&
|
||||
response.data.nip94_event.tags.length
|
||||
) {
|
||||
// Return first 'url' tag we find on the returned nip94 event
|
||||
const imageUrl = response.data.nip94_event.tags.find(
|
||||
(item) => item[0] === 'url'
|
||||
)
|
||||
|
||||
if (imageUrl) return imageUrl[1]
|
||||
}
|
||||
|
||||
throw new BaseError(HandledErrorType.NOSTR_CHECK_BAD_EVENT, {
|
||||
context: { ...response.data }
|
||||
})
|
||||
} catch (error) {
|
||||
// Handle axios errors
|
||||
if (isAxiosError(error)) {
|
||||
if (error.request) {
|
||||
// The request was made but no response was received
|
||||
throw new BaseError(HandledErrorType.AXIOS_REQ, {
|
||||
cause: error
|
||||
})
|
||||
} else if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
// nostrcheck-server can return different results, including message or description
|
||||
const data = error.response.data
|
||||
let message = error.message
|
||||
if (data) {
|
||||
message = data?.message || data?.description || error.message
|
||||
}
|
||||
throw new BaseError(HandledErrorType.AXIOS_RES + message, {
|
||||
cause: error
|
||||
})
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
throw new BaseError(HandledErrorType.AXIOS_ERR, {
|
||||
cause: error
|
||||
})
|
||||
}
|
||||
} else if (error instanceof BaseError) {
|
||||
throw error
|
||||
} else {
|
||||
throw handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auth = async () => {
|
||||
try {
|
||||
const url = `${this.#url}${this.#media}`
|
||||
|
||||
let hexPubkey: string | undefined
|
||||
const userState = store.getState().user
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
hexPubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hexPubkey) {
|
||||
throw new BaseError(HandledErrorType.PUBKEY)
|
||||
}
|
||||
|
||||
const unsignedEvent: NostrEvent = {
|
||||
content: '',
|
||||
created_at: now(),
|
||||
kind: NDKKind.HttpAuth,
|
||||
pubkey: hexPubkey,
|
||||
tags: [
|
||||
['u', url],
|
||||
['method', 'POST']
|
||||
]
|
||||
}
|
||||
|
||||
const signedEvent = await window.nostr?.signEvent(unsignedEvent)
|
||||
return btoa(JSON.stringify(signedEvent))
|
||||
} catch (error) {
|
||||
if (error instanceof BaseError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new BaseError(HandledErrorType.SIGN, {
|
||||
cause: handleError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { MediaOperations } from '.'
|
||||
|
||||
export class route96 implements MediaOperations {
|
||||
post = () => {
|
||||
throw new Error('route96 post Not implemented.')
|
||||
}
|
||||
}
|
@ -1,2 +1 @@
|
||||
export * from './zap'
|
||||
export * from './image'
|
||||
|
@ -4,11 +4,8 @@ export * from './useFilteredMods'
|
||||
export * from './useGames'
|
||||
export * from './useMuteLists'
|
||||
export * from './useNSFWList'
|
||||
export * from './useRepostList'
|
||||
export * from './useReactions'
|
||||
export * from './useNDKContext'
|
||||
export * from './useScrollDisable'
|
||||
export * from './useLocalStorage'
|
||||
export * from './useSessionStorage'
|
||||
export * from './useLocalCache'
|
||||
export * from './useReplies'
|
||||
|
@ -13,15 +13,14 @@ import { useNDKContext } from './useNDKContext'
|
||||
|
||||
export const useComments = (
|
||||
author: string | undefined,
|
||||
aTag: string | undefined,
|
||||
eTag?: string | undefined
|
||||
aTag: string | undefined
|
||||
) => {
|
||||
const { ndk } = useNDKContext()
|
||||
const [commentEvents, setCommentEvents] = useState<CommentEvent[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!(author && (aTag || eTag))) {
|
||||
// Author and aTag/eTag are required
|
||||
if (!(author && aTag)) {
|
||||
// Author and aTag are required
|
||||
return
|
||||
}
|
||||
|
||||
@ -40,7 +39,7 @@ export const useComments = (
|
||||
})
|
||||
.catch((err) => {
|
||||
log(
|
||||
false, // Too many failed requests, turned off for clarity
|
||||
true,
|
||||
LogType.Error,
|
||||
`An error occurred in fetching user's (${author}) ${UserRelaysType.Read}`,
|
||||
err
|
||||
@ -49,17 +48,8 @@ export const useComments = (
|
||||
})
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [NDKKind.Text, NDKKind.GenericReply],
|
||||
...(aTag
|
||||
? {
|
||||
'#a': [aTag]
|
||||
}
|
||||
: {}),
|
||||
...(eTag
|
||||
? {
|
||||
'#e': [eTag]
|
||||
}
|
||||
: {})
|
||||
kinds: [NDKKind.Text],
|
||||
'#a': [aTag]
|
||||
}
|
||||
|
||||
const relayUrls = new Set<string>()
|
||||
@ -83,11 +73,21 @@ export const useComments = (
|
||||
|
||||
subscription.on('event', (ndkEvent) => {
|
||||
setCommentEvents((prev) => {
|
||||
if (prev.find((e) => e.event.id === ndkEvent.id)) {
|
||||
if (prev.find((e) => e.id === ndkEvent.id)) {
|
||||
return [...prev]
|
||||
}
|
||||
|
||||
return [{ event: ndkEvent }, ...prev]
|
||||
const commentEvent: CommentEvent = {
|
||||
kind: NDKKind.Text,
|
||||
tags: ndkEvent.tags,
|
||||
content: ndkEvent.content,
|
||||
created_at: ndkEvent.created_at!,
|
||||
pubkey: ndkEvent.pubkey,
|
||||
id: ndkEvent.id,
|
||||
sig: ndkEvent.sig!
|
||||
}
|
||||
|
||||
return [commentEvent, ...prev]
|
||||
})
|
||||
})
|
||||
|
||||
@ -102,7 +102,7 @@ export const useComments = (
|
||||
subscription.stop()
|
||||
}
|
||||
}
|
||||
}, [aTag, author, eTag, ndk])
|
||||
}, [aTag, author, ndk])
|
||||
|
||||
return {
|
||||
commentEvents,
|
||||
|
@ -112,7 +112,7 @@ export const useFilteredMods = (
|
||||
case WOTFilterOptions.Site_And_Mine:
|
||||
return mods.filter(
|
||||
(mod) =>
|
||||
isInWoT(siteWot, siteWotLevel, mod.author) &&
|
||||
isInWoT(siteWot, siteWotLevel, mod.author) ||
|
||||
isInWoT(userWot, userWotLevel, mod.author)
|
||||
)
|
||||
}
|
||||
@ -128,19 +128,10 @@ export const useFilteredMods = (
|
||||
npubToHex(userState.user.npub as string) === author
|
||||
const isUnmoderatedFully =
|
||||
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
|
||||
const isOnlyBlocked =
|
||||
filterOptions.moderated === ModeratedFilter.Only_Blocked
|
||||
|
||||
if (isOnlyBlocked && isAdmin) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
muteLists.admin.authors.includes(mod.author) ||
|
||||
muteLists.admin.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
} else if (isUnmoderatedFully && (isAdmin || isOwner)) {
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
// Allow "Unmoderated Fully" when author visits own profile
|
||||
} else {
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
// Allow "Unmoderated Fully" when author visits own profile
|
||||
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.admin.authors.includes(mod.author) &&
|
||||
|
@ -1,37 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { setLocalStorageItem, removeLocalStorageItem } from 'utils'
|
||||
|
||||
export function useLocalCache<T>(
|
||||
key: string
|
||||
): [
|
||||
T | undefined,
|
||||
React.Dispatch<React.SetStateAction<T | undefined>>,
|
||||
() => void
|
||||
] {
|
||||
const [cache, setCache] = useState<T | undefined>(() => {
|
||||
const storedValue = window.localStorage.getItem(key)
|
||||
if (storedValue === null) return undefined
|
||||
|
||||
// Parse the value
|
||||
const parsedStoredValue = JSON.parse(storedValue)
|
||||
return parsedStoredValue
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (cache) {
|
||||
setLocalStorageItem(key, JSON.stringify(cache))
|
||||
} else {
|
||||
removeLocalStorageItem(key)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}, [cache, key])
|
||||
|
||||
const clearCache = useCallback(() => {
|
||||
setCache(undefined)
|
||||
}, [])
|
||||
|
||||
return [cache, setCache, clearCache]
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
getLocalStorageItem,
|
||||
mergeWithInitialValue,
|
||||
removeLocalStorageItem,
|
||||
setLocalStorageItem
|
||||
} from 'utils'
|
||||
@ -11,6 +10,17 @@ const useLocalStorageSubscribe = (callback: () => void) => {
|
||||
return () => window.removeEventListener('storage', callback)
|
||||
}
|
||||
|
||||
function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
|
||||
if (
|
||||
!Array.isArray(storedValue) &&
|
||||
typeof storedValue === 'object' &&
|
||||
storedValue !== null
|
||||
) {
|
||||
return { ...initialValue, ...storedValue }
|
||||
}
|
||||
return storedValue
|
||||
}
|
||||
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
|
@ -70,16 +70,12 @@ export const useReactions = (params: UseReactionsParams) => {
|
||||
}, [reactionEvents, userState])
|
||||
|
||||
const getPubkey = async () => {
|
||||
let hexPubkey: string | undefined
|
||||
let hexPubkey: string
|
||||
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
hexPubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!hexPubkey) {
|
||||
|
@ -1,53 +0,0 @@
|
||||
import {
|
||||
NDKEvent,
|
||||
NDKKind,
|
||||
NDKSubscriptionCacheUsage
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
import { useState } from 'react'
|
||||
import { useNDKContext } from './useNDKContext'
|
||||
import { useDidMount } from './useDidMount'
|
||||
|
||||
export const useReplies = (eTag: string | undefined) => {
|
||||
const { ndk } = useNDKContext()
|
||||
const [replies, setReplies] = useState<NDKEvent[]>([])
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
|
||||
useDidMount(async () => {
|
||||
if (!eTag) {
|
||||
setIsComplete(true)
|
||||
return
|
||||
}
|
||||
|
||||
let eDepth: string | undefined = eTag
|
||||
while (eDepth) {
|
||||
const previousReply = await ndk.fetchEvent(
|
||||
{
|
||||
kinds: [NDKKind.Text, NDKKind.GenericReply],
|
||||
ids: [eDepth]
|
||||
},
|
||||
{
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
|
||||
}
|
||||
)
|
||||
if (previousReply) {
|
||||
setReplies((p) => {
|
||||
if (p.findIndex((p) => p.id === previousReply.id) === -1) {
|
||||
p.push(previousReply)
|
||||
}
|
||||
return p
|
||||
})
|
||||
eDepth = previousReply.tagValue('e')
|
||||
} else {
|
||||
eDepth = undefined
|
||||
}
|
||||
}
|
||||
setIsComplete(true)
|
||||
})
|
||||
|
||||
return {
|
||||
size: replies.length,
|
||||
isComplete,
|
||||
parent: replies.length > 0 ? replies[0] : undefined,
|
||||
root: isComplete ? replies[replies.length - 1] : undefined
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { useDidMount } from './useDidMount'
|
||||
import { useNDKContext } from './useNDKContext'
|
||||
import { CurationSetIdentifiers, getReportingSet } from 'utils'
|
||||
|
||||
export const useRepostList = () => {
|
||||
const ndkContext = useNDKContext()
|
||||
const [repostList, setRepostList] = useState<string[]>([])
|
||||
|
||||
useDidMount(async () => {
|
||||
const list = await getReportingSet(
|
||||
CurationSetIdentifiers.Repost,
|
||||
ndkContext
|
||||
)
|
||||
setRepostList(list)
|
||||
})
|
||||
|
||||
return repostList
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
getSessionStorageItem,
|
||||
mergeWithInitialValue,
|
||||
removeSessionStorageItem,
|
||||
setSessionStorageItem
|
||||
} from 'utils'
|
||||
@ -11,6 +10,17 @@ const useSessionStorageSubscribe = (callback: () => void) => {
|
||||
return () => window.removeEventListener('sessionStorage', callback)
|
||||
}
|
||||
|
||||
function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
|
||||
if (
|
||||
!Array.isArray(storedValue) &&
|
||||
typeof storedValue === 'object' &&
|
||||
storedValue !== null
|
||||
) {
|
||||
return { ...initialValue, ...storedValue }
|
||||
}
|
||||
return storedValue
|
||||
}
|
||||
|
||||
export function useSessionStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { MAX_VISIBLE_TEXT_PER_COMMENT } from '../constants'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useTextLimit = (
|
||||
text: string,
|
||||
limit: number = MAX_VISIBLE_TEXT_PER_COMMENT
|
||||
) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const isTextOverflowing = text.length > limit
|
||||
const updated =
|
||||
isExpanded || !isTextOverflowing ? text : text.slice(0, limit) + '…'
|
||||
|
||||
return {
|
||||
text: updated,
|
||||
isTextOverflowing,
|
||||
isExpanded,
|
||||
toggle: () => setIsExpanded((prev) => !prev)
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import styles from '../styles/footer.module.scss'
|
||||
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
@ -9,7 +7,6 @@ export const Footer = () => {
|
||||
<p className={styles.secMainFooterPara}>
|
||||
Built with
|
||||
<a
|
||||
rel='noopener'
|
||||
className={styles.secMainFooterParaLink}
|
||||
href='https://github.com/nostr-protocol/nostr'
|
||||
target='_blank'
|
||||
@ -17,26 +14,21 @@ export const Footer = () => {
|
||||
Nostr
|
||||
</a>{' '}
|
||||
by
|
||||
<Link
|
||||
<a
|
||||
className={styles.secMainFooterParaLink}
|
||||
to={getProfilePageRoute(
|
||||
'nprofile1qqsre6jgq6c7r2vzn5cdtju20qq36sn3cer5avc4x8kfru5pzrlr7sqnancjp'
|
||||
)}
|
||||
href='https://degmods.com/profile/nprofile1qqsre6jgq6c7r2vzn5cdtju20qq36sn3cer5avc4x8kfru5pzrlr7sqnancjp'
|
||||
target='_blank'
|
||||
>
|
||||
Freakoverse
|
||||
</Link>
|
||||
</a>
|
||||
, with the support of{' '}
|
||||
<Link
|
||||
className={styles.secMainFooterParaLink}
|
||||
to={appRoutes.supporters}
|
||||
>
|
||||
<a className={styles.secMainFooterParaLink} href='backers.html'>
|
||||
Supporters
|
||||
</Link>
|
||||
</a>
|
||||
. Check our
|
||||
<Link className={styles.secMainFooterParaLink} to={appRoutes.backup}>
|
||||
<a className={styles.secMainFooterParaLink} href='backup.html'>
|
||||
Backup Plan
|
||||
</Link>
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
launch as launchNostrLoginDialog
|
||||
} from 'nostr-login'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Link, useRevalidator } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Banner } from '../components/Banner'
|
||||
import { ZapPopUp } from '../components/Zap'
|
||||
import {
|
||||
@ -22,13 +22,12 @@ import { npubToHex } from '../utils'
|
||||
import logo from '../assets/img/DEG Mods Logo With Text.svg'
|
||||
import placeholder from '../assets/img/DEG Mods Default PP.png'
|
||||
import { resetUserWot } from 'store/reducers/wot'
|
||||
import { NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||
|
||||
export const Header = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { findMetadata, ndk } = useNDKContext()
|
||||
const { findMetadata } = useNDKContext()
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const revalidator = useRevalidator()
|
||||
|
||||
// Track nostr-login extension modal open state
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const handleOpen = () => setIsOpen(true)
|
||||
@ -51,7 +50,6 @@ export const Header = () => {
|
||||
dispatch(setAuth(null))
|
||||
dispatch(setUser(null))
|
||||
dispatch(resetUserWot())
|
||||
ndk.signer = undefined
|
||||
} else {
|
||||
dispatch(
|
||||
setAuth({
|
||||
@ -65,7 +63,6 @@ export const Header = () => {
|
||||
pubkey: npubToHex(npub)!
|
||||
})
|
||||
)
|
||||
ndk.signer = new NDKNip07Signer()
|
||||
findMetadata(npub).then((userProfile) => {
|
||||
if (userProfile) {
|
||||
dispatch(
|
||||
@ -78,12 +75,8 @@ export const Header = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// React router - revalidate loader states on auth changes
|
||||
revalidator.revalidate()
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, findMetadata])
|
||||
|
||||
const handleLogin = () => {
|
||||
@ -443,21 +436,6 @@ const RegisterButtonWithDialog = () => {
|
||||
nos2x
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
className='labelDescriptionMain'
|
||||
style={{
|
||||
padding: '10px',
|
||||
borderRadius: '10px',
|
||||
background: 'rgba(205,44,255,0.1)',
|
||||
border: 'solid 2px rgba(255,66,235,0.3)',
|
||||
margin: '10px 0 0 0',
|
||||
color: '#ffffffbf'
|
||||
}}
|
||||
>
|
||||
Warning: Make sure you backup your private key
|
||||
somewhere safe. If you lose it or it gets leaked, we
|
||||
actually can't help you.
|
||||
</p>
|
||||
<p
|
||||
className='labelDescriptionMain'
|
||||
style={{
|
||||
|
@ -44,7 +44,7 @@ export const Layout = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [dispatch, ndk])
|
||||
}, [ndk, dispatch])
|
||||
|
||||
// calculate user's wot
|
||||
useEffect(() => {
|
||||
@ -60,7 +60,7 @@ export const Layout = () => {
|
||||
toast.error('An error occurred in calculating user web-of-trust!')
|
||||
})
|
||||
}
|
||||
}, [dispatch, ndk, userState.user?.pubkey])
|
||||
}, [ndk, userState.user, dispatch])
|
||||
|
||||
// get site's wot level
|
||||
useEffect(() => {
|
||||
@ -106,7 +106,7 @@ export const Layout = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [dispatch, fetchEventFromUserRelays, userState.user?.pubkey])
|
||||
}, [userState.user, dispatch, fetchEventFromUserRelays])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,37 +0,0 @@
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { LoaderFunctionArgs, redirect } from 'react-router-dom'
|
||||
import { CommentsLoaderResult } from 'types/comments'
|
||||
import { log, LogType } from 'utils'
|
||||
|
||||
export const commentsLoader =
|
||||
(ndkContext: NDKContextType) =>
|
||||
async ({ params }: LoaderFunctionArgs) => {
|
||||
const { nevent } = params
|
||||
|
||||
if (!nevent) {
|
||||
log(true, LogType.Error, 'Required nevent.')
|
||||
return redirect('..')
|
||||
}
|
||||
|
||||
try {
|
||||
const replyEvent = await ndkContext.ndk.fetchEvent(nevent)
|
||||
|
||||
if (!replyEvent) {
|
||||
throw new Error('We are unable to find the comment on the relays')
|
||||
}
|
||||
|
||||
const result: Partial<CommentsLoaderResult> = {
|
||||
event: replyEvent
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
let message = 'An error occurred in fetching comment from relays'
|
||||
log(true, LogType.Error, message, error)
|
||||
if (error instanceof Error) {
|
||||
message = error.message
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
return redirect('..')
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Link, useLocation, useRouteError } from 'react-router-dom'
|
||||
import { Link, useRouteError } from 'react-router-dom'
|
||||
import { appRoutes } from 'routes'
|
||||
|
||||
interface NotFoundPageProps {
|
||||
@ -12,8 +12,6 @@ export const NotFoundPage = ({
|
||||
}: Partial<NotFoundPageProps>) => {
|
||||
const error = useRouteError() as Partial<NotFoundPageProps>
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
@ -25,19 +23,7 @@ export const NotFoundPage = ({
|
||||
<div>
|
||||
<p>{error?.message || message}</p>
|
||||
</div>
|
||||
<div
|
||||
className='IBMSMAction'
|
||||
style={{
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={location.pathname}
|
||||
className='btn btnMain IBMSMActionBtn'
|
||||
type='button'
|
||||
>
|
||||
Try again
|
||||
</Link>
|
||||
<div className='IBMSMAction'>
|
||||
<Link
|
||||
to={appRoutes.home}
|
||||
className='btn btnMain IBMSMActionBtn'
|
||||
|
@ -13,22 +13,14 @@ export type FAQItem = {
|
||||
const FAQ_ITEMS: FAQItem[] = [
|
||||
{
|
||||
question: "You don't host mod files?",
|
||||
answer: `We could, but that's not the focus. When a creator publishes a mod, they have the option to upload their mod files
|
||||
to whichever server they want and simply add the link to their post. To the end user, there's no difference; they'd always see
|
||||
a 'Download' button. This results in the censorship-resistant structure of a creator's mod post. However, later on, we'll be
|
||||
implementing a system where creators can upload within the site (not to the site) and choose from multiple server hosts
|
||||
(to upload to one or more), with us potentially offering a backup. This system would also provide censorship resistance to
|
||||
the mod files themselves, as anyone would be able to host the files, back them up, and provide them for the public to download from,
|
||||
decreasing the chance of the file getting taken down or lost.`
|
||||
answer: `We don't handle that directly, but you, as the creator, will.`
|
||||
},
|
||||
{
|
||||
question:
|
||||
'How do you assure security of game mod files that someone downloads?',
|
||||
answer: `When a mod creator attempts to publish a mod, there is a security field that they can fill out
|
||||
that shows a scan report of the files, and users would be able to see that report. If a mod creator doesn't add a report,
|
||||
a prominent warning would be shown to the user on a mod's post (TBA, if not already added). Later, when the new file uploading
|
||||
system (Blossom) gets implemented, along with further security implementations, a scan would automatically happen (depending on
|
||||
the server that it is being uploaded to), and a report would be auto-generated and shared within the mod post.`
|
||||
answer: `We don't assure security directly. However, we will provide a reaction
|
||||
system to help users gauge the safety of download links, and mod creators
|
||||
are encouraged to include scan links.`
|
||||
},
|
||||
{
|
||||
question: "Why are you quoting 'account'?",
|
||||
@ -49,11 +41,8 @@ const FAQ_ITEMS: FAQItem[] = [
|
||||
{
|
||||
question:
|
||||
"You can't do anything about any mod or person? Nothing at all? What about the illegal stuff?",
|
||||
answer: `While we can't directly take down mod posts or ban user accounts, the best that we can do is hide
|
||||
posts from initially being viewed on the site. However, they can still be accessible if a user has a direct
|
||||
link, and they can also be accessible on different sites running the same protocol and similar setup to DEG Mods.
|
||||
When non-mods, harmful, or illegal posts are published, such posts would be discovered and then hidden.
|
||||
Afterwards, relevant authorities would handle the rest.`
|
||||
answer: `Direct removal or banning is not possible. We can only filter or
|
||||
hide content on the site, but it remains accessible on here and elsewhere.`
|
||||
},
|
||||
{
|
||||
question:
|
||||
@ -68,12 +57,13 @@ const FAQ_ITEMS: FAQItem[] = [
|
||||
{
|
||||
question: 'Is this an open-source project?',
|
||||
answer: `Yes, DEG Mods is open-source. You can access the code repository
|
||||
here (sharing soon).`
|
||||
[here](https://github.com/your-repo).`
|
||||
},
|
||||
{
|
||||
question: "Who's developing / maintaining DEG Mods?",
|
||||
answer: `Considering this is an open-source project, anyone can contribute to its development and maintenance.
|
||||
With that said, the project was ideated, launched, and led by Freakoverse.`
|
||||
With that said, the initial idea-tor, designer, and frontend developer is [Freakoverse](https://degmods.com/profile/nprofile1qqsre6jgq6c7r2vzn5cdtju20qq36sn3cer5avc4x8kfru5pzrlr7sqnancjp), and the co-developer
|
||||
is [Nostr Dev](https://nostrdev.com/).`
|
||||
},
|
||||
{
|
||||
question: "Who's that character above with the orange hair?",
|
||||
@ -81,7 +71,9 @@ const FAQ_ITEMS: FAQItem[] = [
|
||||
},
|
||||
{
|
||||
question: "Who's that character above with the purple hair?",
|
||||
answer: `That's Moda-chan. DEG Mods' mascot. She's a game mod creator!`
|
||||
answer: `That's Moda-chan. DEG Mods' mascot. She's a master game mod creator! (Yes, she was AI-generated,
|
||||
as such her design is temporary and will be replaced with a design created by an artist (or artists)
|
||||
when that time comes.)`
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -1,126 +0,0 @@
|
||||
import { capitalizeEachWord } from 'utils'
|
||||
import '../styles/backup.css'
|
||||
import backupPlanImg from '../assets/img/DEG Mods Backup Plan.png'
|
||||
// import placeholder from '../assets/img/DEGMods Placeholder Img.png'
|
||||
interface BackupItemProps {
|
||||
name: string
|
||||
image: string
|
||||
link: string
|
||||
type: 'repo' | 'alt' | 'exe'
|
||||
}
|
||||
const BACKUP_LIST: BackupItemProps[] = [
|
||||
// {
|
||||
// name: 'Github',
|
||||
// type: 'repo',
|
||||
// image:
|
||||
// 'https://www.c-sharpcorner.com/article/create-github-repository-and-add-newexisting-project-using-github-desktop/Images/github.png',
|
||||
// link: '#'
|
||||
// },
|
||||
// {
|
||||
// name: 'Github, but nostr',
|
||||
// type: 'repo',
|
||||
// image: 'https://vitorpamplona.com/images/nostr.gif',
|
||||
// link: '#'
|
||||
// },
|
||||
// {
|
||||
// name: 'name',
|
||||
// type: 'alt',
|
||||
// image: placeholder,
|
||||
// link: '#'
|
||||
// },
|
||||
// {
|
||||
// name: '',
|
||||
// type: 'exe',
|
||||
// image: placeholder,
|
||||
// link: '#'
|
||||
// }
|
||||
]
|
||||
|
||||
const BackupItem = ({ name, image, link, type }: BackupItemProps) => {
|
||||
return (
|
||||
<a
|
||||
className='backupListLink'
|
||||
href={link}
|
||||
style={{
|
||||
background: `linear-gradient(15deg, rgba(0,0,0,0.75), rgba(0,0,0,0.25)),
|
||||
url("${image}") center / cover no-repeat,
|
||||
linear-gradient(45deg, rgba(0,0,0,0.1), rgba(255,255,255,0.01) 50%, rgba(0,0,0,0.1))`
|
||||
}}
|
||||
target='_blank'
|
||||
>
|
||||
<div className='backupListLinkInside'>
|
||||
<h3>
|
||||
{type === 'exe' ? type.toUpperCase() : capitalizeEachWord(type)}:{' '}
|
||||
{name}
|
||||
</h3>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export const BackupPage = () => {
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup'>
|
||||
<div className='IBMSecMain'>
|
||||
<div className='AboutSec'>
|
||||
<div className='LearnText'>
|
||||
<div className='LearnTextInside'>
|
||||
<h1
|
||||
className='LearnTextHeading'
|
||||
style={{ textAlign: 'center' }}
|
||||
>
|
||||
Backup Plan: Repos, Alts, EXE
|
||||
</h1>
|
||||
<img alt='' src={backupPlanImg} />
|
||||
<p className='LearnTextPara'>
|
||||
It's pretty clear that authoritarianism and censorship is on
|
||||
the rise, on all fronts, and from what can be seen, any idea
|
||||
that push for the opposite gets attacked. That's why DEG
|
||||
Mods is running on Nostr, and that's why we're also writing
|
||||
this backup plan.
|
||||
<br />
|
||||
</p>
|
||||
<h3 className='LearnTextHeading'>Repositories</h3>
|
||||
<p className='LearnTextPara'>
|
||||
Wherever we can, we'll put DEG Mods' code on multiple
|
||||
repositories such as Github, and (github but on nostr).
|
||||
Below you can find the links where we've uploaded the site's
|
||||
code to.
|
||||
<br />
|
||||
</p>
|
||||
<h3 className='LearnTextHeading'>Alternatives</h3>
|
||||
<p className='LearnTextPara'>
|
||||
With the repositories for DEG Mods is up on multiple places,
|
||||
we encourage people to take the code and duplicate it
|
||||
elsewhere. Fork it, change the design, remove or add systems
|
||||
and features, and make your own version. Below you can find
|
||||
links of alts that we've found.
|
||||
<br />
|
||||
</p>
|
||||
<h3 className='LearnTextHeading'>EXE</h3>
|
||||
<p className='LearnTextPara'>
|
||||
One last push we'd like to do is to create a .exe that'll
|
||||
open up DEG Mods on your PC, as if you've opened the website
|
||||
normally, with almost all of the functionalities you'd
|
||||
expect (if not all). We want to do this so that in case
|
||||
there are no alternatives, or that they're getting shut
|
||||
down, then you can just rely on this instead. The link to it
|
||||
will be added here the moment it becomes available.
|
||||
<br />
|
||||
</p>
|
||||
<div className='backupList'>
|
||||
{BACKUP_LIST.map((b) => (
|
||||
<BackupItem {...b} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -33,15 +33,11 @@ export const blogRouteAction =
|
||||
}
|
||||
|
||||
const userState = store.getState().user
|
||||
let hexPubkey: string | undefined
|
||||
let hexPubkey: string
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
hexPubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!hexPubkey) {
|
||||
|
@ -3,10 +3,7 @@ import {
|
||||
useLoaderData,
|
||||
Link as ReactRouterLink,
|
||||
useNavigation,
|
||||
useSubmit,
|
||||
Outlet,
|
||||
useParams,
|
||||
useNavigate
|
||||
useSubmit
|
||||
} from 'react-router-dom'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { ProfileSection } from 'components/ProfileSection'
|
||||
@ -18,12 +15,9 @@ import { Interactions } from 'components/Internal/Interactions'
|
||||
import { BlogCard } from 'components/BlogCard'
|
||||
import { copyTextToClipboard } from 'utils'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useAppSelector, useBodyScrollDisable, useLocalStorage } from 'hooks'
|
||||
import { useAppSelector, useBodyScrollDisable } from 'hooks'
|
||||
import { ReportPopup } from 'components/ReportPopup'
|
||||
import { Viewer } from 'components/Markdown/Viewer'
|
||||
import { PostWarnings } from 'components/PostWarning'
|
||||
import { appRoutes } from 'routes'
|
||||
import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
|
||||
|
||||
const BLOG_REPORT_REASONS = [
|
||||
{ label: 'Actually CP', key: 'actuallyCP' },
|
||||
@ -35,14 +29,12 @@ const BLOG_REPORT_REASONS = [
|
||||
]
|
||||
|
||||
export const BlogPage = () => {
|
||||
const { nevent } = useParams()
|
||||
const { blog, latest, isAddedToNSFW, isBlocked, postWarning } =
|
||||
const { blog, latest, isAddedToNSFW, isBlocked } =
|
||||
useLoaderData() as BlogPageLoaderResult
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const isAdmin =
|
||||
userState.user?.npub &&
|
||||
userState.user.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isLoggedIn = userState.auth && userState.user?.pubkey !== 'undefined'
|
||||
const navigation = useNavigation()
|
||||
const [commentCount, setCommentCount] = useState(0)
|
||||
|
||||
@ -82,17 +74,6 @@ export const BlogPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const navigate = useNavigate()
|
||||
const [confirmNsfw] = useLocalStorage<boolean>('confirm-nsfw', false)
|
||||
const [showNsfwPopup, setShowNsfwPopup] = useState<boolean>(
|
||||
(blog?.nsfw ?? false) && !confirmNsfw
|
||||
)
|
||||
const handleConfirm = (confirm: boolean) => {
|
||||
if (!confirm) {
|
||||
navigate(appRoutes.home)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
@ -102,7 +83,6 @@ export const BlogPage = () => {
|
||||
<>
|
||||
<div className='IBMSMSplitMainBigSide'>
|
||||
<div className='IBMSMSplitMainBigSideSec'>
|
||||
{postWarning && <PostWarnings type={postWarning} />}
|
||||
<div className='IBMSMSMBSSPost'>
|
||||
<div
|
||||
className='dropdown dropdownMain dropdownMainBlogpost'
|
||||
@ -192,25 +172,23 @@ export const BlogPage = () => {
|
||||
Share
|
||||
</a>
|
||||
|
||||
{isLoggedIn && (
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
id='reportPost'
|
||||
onClick={() => setShowReportPopUp(Date.now())}
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
id='reportPost'
|
||||
onClick={() => setShowReportPopUp(Date.now())}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
||||
</svg>
|
||||
Report
|
||||
</a>
|
||||
)}
|
||||
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
||||
</svg>
|
||||
Report
|
||||
</a>
|
||||
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
@ -329,13 +307,6 @@ export const BlogPage = () => {
|
||||
</>
|
||||
)}
|
||||
{!!blog?.author && <ProfileSection pubkey={blog.author} />}
|
||||
<Outlet key={nevent} />
|
||||
{showNsfwPopup && (
|
||||
<NsfwAlertPopup
|
||||
handleConfirm={handleConfirm}
|
||||
handleClose={() => setShowNsfwPopup(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
} from 'types'
|
||||
import {
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
getFallbackPubkey,
|
||||
getLocalStorageItem,
|
||||
log,
|
||||
LogType
|
||||
@ -42,9 +41,7 @@ export const blogRouteLoader =
|
||||
}
|
||||
|
||||
const userState = store.getState().user
|
||||
const loggedInUserPubkey =
|
||||
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const loggedInUserPubkey = userState?.user?.pubkey as string | undefined
|
||||
|
||||
// Check if editing and the user is the original author
|
||||
// Redirect if NOT
|
||||
@ -94,7 +91,6 @@ export const blogRouteLoader =
|
||||
])
|
||||
const result: BlogPageLoaderResult = {
|
||||
blog: undefined,
|
||||
event: undefined,
|
||||
latest: [],
|
||||
isAddedToNSFW: false,
|
||||
isBlocked: false
|
||||
@ -103,9 +99,6 @@ export const blogRouteLoader =
|
||||
// Check the blog event result
|
||||
const fetchEventResult = settled[0]
|
||||
if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) {
|
||||
// Save original event
|
||||
result.event = fetchEventResult.value
|
||||
|
||||
// Extract the blog details from the event
|
||||
result.blog = extractBlogDetails(fetchEventResult.value)
|
||||
} else if (fetchEventResult.status === 'rejected') {
|
||||
@ -123,7 +116,7 @@ export const blogRouteLoader =
|
||||
throw new Error('We are unable to find the blog on the relays')
|
||||
}
|
||||
|
||||
// Check the latest blog events
|
||||
// Check the lateast blog events
|
||||
const fetchEventsResult = settled[1]
|
||||
if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) {
|
||||
// Extract the blog card details from the events
|
||||
@ -143,28 +136,10 @@ export const blogRouteLoader =
|
||||
if (muteLists.status === 'fulfilled' && muteLists.value) {
|
||||
if (muteLists && muteLists.value) {
|
||||
if (result.blog && result.blog.aTag) {
|
||||
// Show user or admin post warning if any mute list includes either post or author
|
||||
if (
|
||||
muteLists.value.user.replaceableEvents.includes(
|
||||
result.blog.aTag
|
||||
) ||
|
||||
(result.blog.author &&
|
||||
muteLists.value.user.authors.includes(result.blog.author))
|
||||
) {
|
||||
result.postWarning = 'user'
|
||||
}
|
||||
|
||||
if (
|
||||
muteLists.value.admin.replaceableEvents.includes(
|
||||
result.blog.aTag
|
||||
) ||
|
||||
(result.blog.author &&
|
||||
muteLists.value.admin.authors.includes(result.blog.author))
|
||||
) {
|
||||
result.postWarning = 'admin'
|
||||
}
|
||||
|
||||
if (
|
||||
muteLists.value.user.replaceableEvents.includes(result.blog.aTag)
|
||||
) {
|
||||
result.isBlocked = true
|
||||
@ -172,6 +147,8 @@ export const blogRouteLoader =
|
||||
}
|
||||
|
||||
// Moderate the latest
|
||||
const isAdmin =
|
||||
userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isOwner =
|
||||
userState.user?.pubkey && userState.user.pubkey === pubkey
|
||||
const isUnmoderatedFully =
|
||||
|
@ -14,8 +14,7 @@ import {
|
||||
useLocalStorage,
|
||||
useMuteLists,
|
||||
useNDKContext,
|
||||
useNSFWList,
|
||||
useRepostList
|
||||
useNSFWList
|
||||
} from '../hooks'
|
||||
import { appRoutes, getModPageRoute } from '../routes'
|
||||
import { BlogCardDetails, ModDetails, NSFWFilter, SortBy } from '../types'
|
||||
@ -258,14 +257,18 @@ const DisplayLatestMods = () => {
|
||||
|
||||
const muteLists = useMuteLists()
|
||||
const nsfwList = useNSFWList()
|
||||
const repostList = useRepostList()
|
||||
|
||||
useDidMount(() => {
|
||||
fetchMods({ source: window.location.host })
|
||||
.then((mods) => {
|
||||
// Sort by the latest (published_at descending)
|
||||
mods.sort((a, b) => b.published_at - a.published_at)
|
||||
setLatestMods(mods)
|
||||
const wotFilteredMods = mods.filter(
|
||||
(mod) =>
|
||||
isInWoT(siteWot, siteWotLevel, mod.author) ||
|
||||
isInWoT(userWot, userWotLevel, mod.author)
|
||||
)
|
||||
setLatestMods(wotFilteredMods)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetchingLatestMods(false)
|
||||
@ -284,36 +287,11 @@ const DisplayLatestMods = () => {
|
||||
!mutedAuthors.includes(mod.author) &&
|
||||
!mutedEvents.includes(mod.aTag) &&
|
||||
!nsfwList.includes(mod.aTag) &&
|
||||
!mod.nsfw &&
|
||||
isInWoT(siteWot, siteWotLevel, mod.author) &&
|
||||
isInWoT(userWot, userWotLevel, mod.author)
|
||||
!mod.nsfw
|
||||
)
|
||||
|
||||
// Add repost tag if missing
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
const mod = filtered[i]
|
||||
const isMissingRepostTag =
|
||||
!mod.repost && mod.aTag && repostList.includes(mod.aTag)
|
||||
|
||||
if (isMissingRepostTag) {
|
||||
mod.repost = true
|
||||
}
|
||||
}
|
||||
|
||||
return filtered.slice(0, 4)
|
||||
}, [
|
||||
latestMods,
|
||||
muteLists.admin.authors,
|
||||
muteLists.admin.replaceableEvents,
|
||||
muteLists.user.authors,
|
||||
muteLists.user.replaceableEvents,
|
||||
nsfwList,
|
||||
repostList,
|
||||
siteWot,
|
||||
siteWotLevel,
|
||||
userWot,
|
||||
userWotLevel
|
||||
])
|
||||
}, [muteLists, nsfwList, latestMods])
|
||||
|
||||
return (
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
|
@ -43,15 +43,11 @@ export const modRouteAction =
|
||||
}
|
||||
|
||||
const userState = store.getState().user
|
||||
let hexPubkey: string | undefined
|
||||
let hexPubkey: string
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
hexPubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!hexPubkey) {
|
||||
|
@ -1,11 +1,9 @@
|
||||
import FsLightbox from 'fslightbox-react'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Outlet,
|
||||
Link as ReactRouterLink,
|
||||
useLoaderData,
|
||||
useNavigate,
|
||||
useNavigation,
|
||||
useParams,
|
||||
useSubmit
|
||||
@ -13,13 +11,8 @@ import {
|
||||
import { toast } from 'react-toastify'
|
||||
import { BlogCard } from '../../components/BlogCard'
|
||||
import { ProfileSection } from '../../components/ProfileSection'
|
||||
import {
|
||||
useAppSelector,
|
||||
useBodyScrollDisable,
|
||||
useDidMount,
|
||||
useLocalStorage
|
||||
} from '../../hooks'
|
||||
import { appRoutes, getGamePageRoute, getModsEditPageRoute } from '../../routes'
|
||||
import { useAppSelector, useBodyScrollDisable } from '../../hooks'
|
||||
import { getGamePageRoute, getModsEditPageRoute } from '../../routes'
|
||||
import '../../styles/comments.css'
|
||||
import '../../styles/downloads.css'
|
||||
import '../../styles/innerPage.css'
|
||||
@ -30,22 +23,12 @@ import '../../styles/styles.css'
|
||||
import '../../styles/tabs.css'
|
||||
import '../../styles/tags.css'
|
||||
import '../../styles/write.css'
|
||||
import {
|
||||
DownloadUrl,
|
||||
ModDetails,
|
||||
ModFormState,
|
||||
ModPageLoaderResult,
|
||||
ModPermissions,
|
||||
MODPERMISSIONS_CONF,
|
||||
MODPERMISSIONS_DESC
|
||||
} from '../../types'
|
||||
import { DownloadUrl, ModPageLoaderResult } from '../../types'
|
||||
import {
|
||||
capitalizeEachWord,
|
||||
checkUrlForFile,
|
||||
copyTextToClipboard,
|
||||
downloadFile,
|
||||
getFilenameFromUrl,
|
||||
isValidUrl
|
||||
getFilenameFromUrl
|
||||
} from '../../utils'
|
||||
import { Comments } from '../../components/comment'
|
||||
import { PublishDetails } from 'components/Internal/PublishDetails'
|
||||
@ -54,10 +37,9 @@ import { ReportPopup } from 'components/ReportPopup'
|
||||
import { Spinner } from 'components/Spinner'
|
||||
import { RouterLoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { OriginalAuthor } from 'components/OriginalAuthor'
|
||||
import DOMPurify from 'dompurify'
|
||||
import TurndownService from 'turndown'
|
||||
import { Viewer } from 'components/Markdown/Viewer'
|
||||
import { PostWarnings } from 'components/PostWarning'
|
||||
import { DownloadDetailsPopup } from 'components/DownloadDetailsPopup'
|
||||
import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
|
||||
|
||||
const MOD_REPORT_REASONS = [
|
||||
{ label: 'Actually CP', key: 'actuallyCP' },
|
||||
@ -71,10 +53,10 @@ const MOD_REPORT_REASONS = [
|
||||
]
|
||||
|
||||
export const ModPage = () => {
|
||||
const { mod, postWarning } = useLoaderData() as ModPageLoaderResult
|
||||
const { mod } = useLoaderData() as ModPageLoaderResult
|
||||
|
||||
// We can get author right away from naddr, no need to wait for mod data
|
||||
const { naddr, nevent } = useParams()
|
||||
const { naddr } = useParams()
|
||||
let author = mod?.author
|
||||
if (naddr && !author) {
|
||||
try {
|
||||
@ -87,14 +69,22 @@ export const ModPage = () => {
|
||||
|
||||
const [commentCount, setCommentCount] = useState(0)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const [confirmNsfw] = useLocalStorage<boolean>('confirm-nsfw', false)
|
||||
const [showNsfwPopup, setShowNsfwPopup] = useState<boolean>(
|
||||
(mod?.nsfw ?? false) && !confirmNsfw
|
||||
)
|
||||
const handleConfirm = (confirm: boolean) => {
|
||||
if (!confirm) {
|
||||
navigate(appRoutes.home)
|
||||
const oldDownloadListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleViewOldLinks = () => {
|
||||
if (oldDownloadListRef.current) {
|
||||
// Toggle styles
|
||||
if (oldDownloadListRef.current.style.height === '0px') {
|
||||
// Enable styles
|
||||
oldDownloadListRef.current.style.padding = ''
|
||||
oldDownloadListRef.current.style.height = ''
|
||||
oldDownloadListRef.current.style.border = ''
|
||||
} else {
|
||||
// Disable styles
|
||||
oldDownloadListRef.current.style.padding = '0'
|
||||
oldDownloadListRef.current.style.height = '0'
|
||||
oldDownloadListRef.current.style.border = 'unset'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,8 +100,18 @@ export const ModPage = () => {
|
||||
<>
|
||||
<div className='IBMSMSplitMainBigSideSec'>
|
||||
<Game />
|
||||
{postWarning && <PostWarnings type={postWarning} />}
|
||||
<Body {...mod} />
|
||||
<Body
|
||||
featuredImageUrl={mod.featuredImageUrl}
|
||||
title={mod.title}
|
||||
body={mod.body}
|
||||
game={mod.game}
|
||||
screenshotsUrls={mod.screenshotsUrls}
|
||||
tags={mod.tags}
|
||||
LTags={mod.LTags}
|
||||
nsfw={mod.nsfw}
|
||||
repost={mod.repost}
|
||||
originalAuthor={mod.originalAuthor}
|
||||
/>
|
||||
<Interactions
|
||||
addressable={mod}
|
||||
commentCount={commentCount}
|
||||
@ -127,23 +127,43 @@ export const ModPage = () => {
|
||||
<h4 className='IBMSMSMBSSDownloadsTitle'>
|
||||
Mod Download
|
||||
</h4>
|
||||
{postWarning && <PostWarnings type={postWarning} />}
|
||||
{mod.downloadUrls.length > 0 && (
|
||||
<div className='IBMSMSMBSSDownloadsPrime'>
|
||||
<Download {...mod.downloadUrls[0]} />
|
||||
</div>
|
||||
)}
|
||||
{mod.downloadUrls.length > 1 && (
|
||||
<div className='IBMSMSMBSSDownloads'>
|
||||
{mod.downloadUrls
|
||||
.slice(1)
|
||||
.map((download, index) => (
|
||||
<Download
|
||||
key={`downloadUrl-${index}`}
|
||||
{...download}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className='IBMSMSMBSSDownloadsActions'>
|
||||
<button
|
||||
className='btn btnMain'
|
||||
id='viewOldLinks'
|
||||
type='button'
|
||||
onClick={handleViewOldLinks}
|
||||
>
|
||||
View other links
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={oldDownloadListRef}
|
||||
id='oldDownloadList'
|
||||
className='IBMSMSMBSSDownloads'
|
||||
style={{
|
||||
padding: 0,
|
||||
height: '0px',
|
||||
border: 'unset'
|
||||
}}
|
||||
>
|
||||
{mod.downloadUrls
|
||||
.slice(1)
|
||||
.map((download, index) => (
|
||||
<Download
|
||||
key={`downloadUrl-${index}`}
|
||||
{...download}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -162,13 +182,6 @@ export const ModPage = () => {
|
||||
{typeof author !== 'undefined' && (
|
||||
<ProfileSection pubkey={author} />
|
||||
)}
|
||||
<Outlet key={nevent} />
|
||||
{showNsfwPopup && (
|
||||
<NsfwAlertPopup
|
||||
handleConfirm={handleConfirm}
|
||||
handleClose={() => setShowNsfwPopup(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -183,7 +196,6 @@ const Game = () => {
|
||||
const { mod, isAddedToNSFW, isBlocked, isRepost } =
|
||||
useLoaderData() as ModPageLoaderResult
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const isLoggedIn = userState.auth && userState.user?.pubkey !== 'undefined'
|
||||
const [showReportPopUp, setShowReportPopUp] = useState<number | undefined>()
|
||||
|
||||
useBodyScrollDisable(!!showReportPopUp)
|
||||
@ -326,27 +338,25 @@ const Game = () => {
|
||||
</svg>
|
||||
Share
|
||||
</a>
|
||||
{isLoggedIn && (
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
id='reportPost'
|
||||
onClick={() => {
|
||||
setShowReportPopUp(Date.now())
|
||||
}}
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
id='reportPost'
|
||||
onClick={() => {
|
||||
setShowReportPopUp(Date.now())
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
||||
</svg>
|
||||
Report
|
||||
</a>
|
||||
)}
|
||||
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
||||
</svg>
|
||||
Report
|
||||
</a>
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={handleBlock}
|
||||
@ -413,22 +423,18 @@ const Game = () => {
|
||||
)
|
||||
}
|
||||
|
||||
type BodyProps = Pick<
|
||||
ModDetails,
|
||||
| 'featuredImageUrl'
|
||||
| 'title'
|
||||
| 'body'
|
||||
| 'game'
|
||||
| 'screenshotsUrls'
|
||||
| 'tags'
|
||||
| 'LTags'
|
||||
| 'nsfw'
|
||||
| 'repost'
|
||||
| 'originalAuthor'
|
||||
| keyof ModPermissions
|
||||
| 'publisherNotes'
|
||||
| 'extraCredits'
|
||||
>
|
||||
type BodyProps = {
|
||||
featuredImageUrl: string
|
||||
title: string
|
||||
body: string
|
||||
game: string
|
||||
screenshotsUrls: string[]
|
||||
tags: string[]
|
||||
LTags: string[]
|
||||
nsfw: boolean
|
||||
repost: boolean
|
||||
originalAuthor?: string
|
||||
}
|
||||
|
||||
const Body = ({
|
||||
featuredImageUrl,
|
||||
@ -440,19 +446,19 @@ const Body = ({
|
||||
LTags,
|
||||
nsfw,
|
||||
repost,
|
||||
originalAuthor,
|
||||
otherAssets,
|
||||
uploadPermission,
|
||||
modPermission,
|
||||
convPermission,
|
||||
assetUsePermission,
|
||||
assetUseComPermission,
|
||||
publisherNotes,
|
||||
extraCredits
|
||||
originalAuthor
|
||||
}: BodyProps) => {
|
||||
const COLLAPSED_MAX_SIZE = 250
|
||||
const postBodyRef = useRef<HTMLDivElement>(null)
|
||||
const viewFullPostBtnRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const markdown = useMemo(() => {
|
||||
const sanitized = DOMPurify.sanitize(body)
|
||||
const turndown = new TurndownService()
|
||||
turndown.keep(['sup', 'sub'])
|
||||
return turndown.turndown(sanitized)
|
||||
}, [body])
|
||||
|
||||
const [lightBoxController, setLightBoxController] = useState({
|
||||
toggler: false,
|
||||
slide: 1
|
||||
@ -502,7 +508,7 @@ const Body = ({
|
||||
padding: '10px 18px'
|
||||
}}
|
||||
>
|
||||
<Viewer markdown={body} />
|
||||
<Viewer markdown={markdown} />
|
||||
<div ref={viewFullPostBtnRef} className='IBMSMSMBSSPostBodyHide'>
|
||||
<div className='IBMSMSMBSSPostBodyHideText'>
|
||||
<p onClick={viewFullPost}>Read Full</p>
|
||||
@ -520,16 +526,6 @@ const Body = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ExtraDetails
|
||||
otherAssets={otherAssets}
|
||||
uploadPermission={uploadPermission}
|
||||
modPermission={modPermission}
|
||||
convPermission={convPermission}
|
||||
assetUsePermission={assetUsePermission}
|
||||
assetUseComPermission={assetUseComPermission}
|
||||
publisherNotes={publisherNotes}
|
||||
extraCredits={extraCredits}
|
||||
/>
|
||||
<div className='IBMSMSMBSSTags'>
|
||||
{nsfw && (
|
||||
<div className='IBMSMSMBSSTagsTag IBMSMSMBSSTagsTagNSFW'>
|
||||
@ -612,29 +608,15 @@ const Body = ({
|
||||
)
|
||||
}
|
||||
|
||||
const Download = (props: DownloadUrl) => {
|
||||
const { url, title, malwareScanLink } = props
|
||||
const Download = ({
|
||||
url,
|
||||
hash,
|
||||
signatureKey,
|
||||
malwareScanLink,
|
||||
modVersion,
|
||||
customNote
|
||||
}: DownloadUrl) => {
|
||||
const [showAuthDetails, setShowAuthDetails] = useState(false)
|
||||
const [showNotice, setShowNotice] = useState(false)
|
||||
const [showScanNotice, setShowCanNotice] = useState(false)
|
||||
|
||||
useDidMount(async () => {
|
||||
const isFile = await checkUrlForFile(url)
|
||||
setShowNotice(!isFile)
|
||||
|
||||
// Check the malware scan url
|
||||
// if it's valid URL
|
||||
// if it contains sha256
|
||||
// if it differs from download link
|
||||
setShowCanNotice(
|
||||
!(
|
||||
malwareScanLink &&
|
||||
isValidUrl(malwareScanLink) &&
|
||||
/\b[a-fA-F0-9]{64}\b/.test(malwareScanLink) &&
|
||||
malwareScanLink !== url
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const handleDownload = () => {
|
||||
// Get the filename from the URL
|
||||
@ -645,9 +627,6 @@ const Download = (props: DownloadUrl) => {
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSDownloadsElement'>
|
||||
{typeof title !== 'undefined' && title !== '' && (
|
||||
<span className='IBMSMSMBSSDownloadsElementDtitle'>{title}</span>
|
||||
)}
|
||||
<div className='IBMSMSMBSSDownloadsElementInside'>
|
||||
<button
|
||||
className='btn btnMain IBMSMSMBSSDownloadsElementBtn'
|
||||
@ -657,25 +636,6 @@ const Download = (props: DownloadUrl) => {
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
{showNotice && (
|
||||
<div className='IBMSMSMBSSNote'>
|
||||
<p>
|
||||
Notice: The creator has provided a download link that doesn't
|
||||
download the files immediately, but rather redirects you to a
|
||||
different site.
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{showScanNotice && (
|
||||
<div className='IBMSMSMBSSWarning'>
|
||||
<p>
|
||||
The mod poster hasn't provided a malware scan report for these
|
||||
files. Be careful.
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/*temporarily commented out the WoT rating for download links within a mod post
|
||||
<div className='IBMSMSMBSSDownloadsElementInside'>
|
||||
<p>Ratings (WIP):</p>
|
||||
@ -850,10 +810,56 @@ const Download = (props: DownloadUrl) => {
|
||||
Authentication Details
|
||||
</p>
|
||||
{showAuthDetails && (
|
||||
<DownloadDetailsPopup
|
||||
{...props}
|
||||
handleClose={() => setShowAuthDetails(false)}
|
||||
/>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTable'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Download URL</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{url}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>SHA-256 hash</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{hash}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Signature from</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{signatureKey}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Scan</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{malwareScanLink}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Mod Version</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{modVersion}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRow'>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst'>
|
||||
<p>Note</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSDownloadsElementInsideAltTableRowCol'>
|
||||
<p>{customNote}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -878,122 +884,3 @@ const DisplayModAuthorBlogs = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ExtraDetailsProps = ModPermissions &
|
||||
Pick<ModFormState, 'publisherNotes' | 'extraCredits'>
|
||||
const ExtraDetails = ({
|
||||
publisherNotes,
|
||||
extraCredits,
|
||||
...rest
|
||||
}: ExtraDetailsProps) => {
|
||||
const extraBoxRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
if (
|
||||
typeof publisherNotes === 'undefined' &&
|
||||
typeof extraCredits === 'undefined' &&
|
||||
Object.values(rest).every((v) => typeof v === 'undefined')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (extraBoxRef.current) {
|
||||
if (extraBoxRef.current.style.display === '') {
|
||||
extraBoxRef.current.style.display = 'none'
|
||||
} else {
|
||||
extraBoxRef.current.style.display = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className='IBMSMSMBSSExtra'>
|
||||
<button
|
||||
className='btn btnMain IBMSMSMBSSExtraBtn'
|
||||
type='button'
|
||||
onClick={handleClick}
|
||||
>
|
||||
Permissions & Details
|
||||
</button>
|
||||
<div
|
||||
className='IBMSMSMBSSExtraBox'
|
||||
ref={extraBoxRef}
|
||||
style={{
|
||||
display: 'none'
|
||||
}}
|
||||
>
|
||||
<div className='IBMSMSMBSSExtraBoxElementWrapper'>
|
||||
{Object.keys(MODPERMISSIONS_CONF).map((k) => {
|
||||
const permKey = k as keyof ModPermissions
|
||||
const confKey = k as keyof typeof MODPERMISSIONS_CONF
|
||||
const modPermission = MODPERMISSIONS_CONF[confKey]
|
||||
const value = rest[permKey]
|
||||
if (typeof value === 'undefined') return null
|
||||
|
||||
const text = MODPERMISSIONS_DESC[`${permKey}_${value}`]
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSExtraBoxElement' key={k}>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
|
||||
<p>{modPermission.header}</p>
|
||||
{value ? (
|
||||
<div className='IBMSMSMBSSExtraBoxElementColMark IBMSMSMBSSExtraBoxElementColMarkGreen'>
|
||||
<svg
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM371.8 211.8C382.7 200.9 382.7 183.1 371.8 172.2C360.9 161.3 343.1 161.3 332.2 172.2L224 280.4L179.8 236.2C168.9 225.3 151.1 225.3 140.2 236.2C129.3 247.1 129.3 264.9 140.2 275.8L204.2 339.8C215.1 350.7 232.9 350.7 243.8 339.8L371.8 211.8z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className='IBMSMSMBSSExtraBoxElementColMark IBMSMSMBSSExtraBoxElementColMarkRed'>
|
||||
<svg
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM175 208.1L222.1 255.1L175 303C165.7 312.4 165.7 327.6 175 336.1C184.4 346.3 199.6 346.3 208.1 336.1L255.1 289.9L303 336.1C312.4 346.3 327.6 346.3 336.1 336.1C346.3 327.6 346.3 312.4 336.1 303L289.9 255.1L336.1 208.1C346.3 199.6 346.3 184.4 336.1 175C327.6 165.7 312.4 165.7 303 175L255.1 222.1L208.1 175C199.6 165.7 184.4 165.7 175 175C165.7 184.4 165.7 199.6 175 208.1V208.1z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
|
||||
<p>
|
||||
{text}
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{typeof publisherNotes !== 'undefined' && publisherNotes !== '' && (
|
||||
<div className='IBMSMSMBSSExtraBoxElement'>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
|
||||
<p>Publisher Notes</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
|
||||
<p>{publisherNotes}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{typeof extraCredits !== 'undefined' && extraCredits !== '' && (
|
||||
<div className='IBMSMSMBSSExtraBoxElement'>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
|
||||
<p>Extra Credits</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
|
||||
<p>{extraCredits}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -16,17 +16,15 @@ import {
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
extractBlogCardDetails,
|
||||
extractModData,
|
||||
getFallbackPubkey,
|
||||
getLocalStorageItem,
|
||||
getReportingSet,
|
||||
log,
|
||||
LogType,
|
||||
timeout
|
||||
LogType
|
||||
} from 'utils'
|
||||
|
||||
export const modRouteLoader =
|
||||
(ndkContext: NDKContextType) =>
|
||||
async ({ params, request }: LoaderFunctionArgs) => {
|
||||
async ({ params }: LoaderFunctionArgs) => {
|
||||
const { naddr } = params
|
||||
if (!naddr) {
|
||||
log(true, LogType.Error, 'Required naddr.')
|
||||
@ -48,17 +46,7 @@ export const modRouteLoader =
|
||||
}
|
||||
|
||||
const userState = store.getState().user
|
||||
const loggedInUserPubkey =
|
||||
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
|
||||
// Check if editing and the user is the original author
|
||||
// Redirect if NOT
|
||||
const url = new URL(request.url)
|
||||
const isEditMode = url.pathname.includes('edit-mod')
|
||||
if (isEditMode && loggedInUserPubkey !== pubkey) {
|
||||
return redirect(appRoutes.mods)
|
||||
}
|
||||
const loggedInUserPubkey = userState?.user?.pubkey as string | undefined
|
||||
|
||||
try {
|
||||
// Set up the filters
|
||||
@ -94,8 +82,8 @@ export const modRouteLoader =
|
||||
|
||||
// Parallel fetch mod event, latest events, mute, nsfw, repost lists
|
||||
const settled = await Promise.allSettled([
|
||||
Promise.race([ndkContext.fetchEvent(modFilter), timeout(2000)]),
|
||||
Promise.race([ndkContext.fetchEvents(latestFilter), timeout(2000)]),
|
||||
ndkContext.fetchEvent(modFilter),
|
||||
ndkContext.fetchEvents(latestFilter),
|
||||
ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users
|
||||
getReportingSet(CurationSetIdentifiers.NSFW, ndkContext),
|
||||
getReportingSet(CurationSetIdentifiers.Repost, ndkContext)
|
||||
@ -103,7 +91,6 @@ export const modRouteLoader =
|
||||
|
||||
const result: ModPageLoaderResult = {
|
||||
mod: undefined,
|
||||
event: undefined,
|
||||
latest: [],
|
||||
isAddedToNSFW: false,
|
||||
isBlocked: false,
|
||||
@ -113,9 +100,6 @@ export const modRouteLoader =
|
||||
// Check the mod event result
|
||||
const fetchEventResult = settled[0]
|
||||
if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) {
|
||||
// Save original event
|
||||
result.event = fetchEventResult.value
|
||||
|
||||
// Extract the mod data from the event
|
||||
result.mod = extractModData(fetchEventResult.value)
|
||||
} else if (fetchEventResult.status === 'rejected') {
|
||||
@ -133,7 +117,7 @@ export const modRouteLoader =
|
||||
throw new Error('We are unable to find the mod on the relays')
|
||||
}
|
||||
|
||||
// Check the latest blog events
|
||||
// Check the lateast blog events
|
||||
const fetchEventsResult = settled[1]
|
||||
if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) {
|
||||
// Extract the blog card details from the events
|
||||
@ -151,27 +135,10 @@ export const modRouteLoader =
|
||||
if (muteLists.status === 'fulfilled' && muteLists.value) {
|
||||
if (muteLists && muteLists.value) {
|
||||
if (result.mod && result.mod.aTag) {
|
||||
// Show user or admin post warning if any mute list includes either post or author
|
||||
if (
|
||||
muteLists.value.user.replaceableEvents.includes(
|
||||
result.mod.aTag
|
||||
) ||
|
||||
muteLists.value.user.authors.includes(result.mod.author)
|
||||
) {
|
||||
result.postWarning = 'user'
|
||||
}
|
||||
|
||||
if (
|
||||
muteLists.value.admin.replaceableEvents.includes(
|
||||
result.mod.aTag
|
||||
) ||
|
||||
muteLists.value.admin.authors.includes(result.mod.author)
|
||||
) {
|
||||
result.postWarning = 'admin'
|
||||
}
|
||||
|
||||
// Check if user has blocked this profile
|
||||
if (
|
||||
muteLists.value.user.replaceableEvents.includes(result.mod.aTag)
|
||||
) {
|
||||
result.isBlocked = true
|
||||
@ -179,6 +146,8 @@ export const modRouteLoader =
|
||||
}
|
||||
|
||||
// Moderate the latest
|
||||
const isAdmin =
|
||||
userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isOwner =
|
||||
userState.user?.pubkey && userState.user.pubkey === pubkey
|
||||
const isUnmoderatedFully =
|
||||
|
@ -1,13 +1,7 @@
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { store } from 'store'
|
||||
import { MuteLists } from 'types'
|
||||
import {
|
||||
getReportingSet,
|
||||
CurationSetIdentifiers,
|
||||
log,
|
||||
LogType,
|
||||
getFallbackPubkey
|
||||
} from 'utils'
|
||||
import { getReportingSet, CurationSetIdentifiers, log, LogType } from 'utils'
|
||||
|
||||
export interface ModsPageLoaderResult {
|
||||
muteLists: {
|
||||
@ -37,11 +31,15 @@ export const modsRouteLoader = (ndkContext: NDKContextType) => async () => {
|
||||
|
||||
// Get the current state
|
||||
const userState = store.getState().user
|
||||
const loggedInUserPubkey =
|
||||
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
|
||||
|
||||
// Check if current user is logged in
|
||||
let userPubkey: string | undefined
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
userPubkey = userState.user.pubkey as string
|
||||
}
|
||||
|
||||
const settled = await Promise.allSettled([
|
||||
ndkContext.getMuteLists(loggedInUserPubkey),
|
||||
ndkContext.getMuteLists(userPubkey),
|
||||
getReportingSet(CurationSetIdentifiers.NSFW, ndkContext),
|
||||
getReportingSet(CurationSetIdentifiers.Repost, ndkContext)
|
||||
])
|
||||
|
@ -30,8 +30,6 @@ import {
|
||||
copyTextToClipboard,
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
extractBlogCardDetails,
|
||||
log,
|
||||
LogType,
|
||||
now,
|
||||
npubToHex,
|
||||
scrollIntoView,
|
||||
@ -48,6 +46,7 @@ export const ProfilePage = () => {
|
||||
profilePubkey,
|
||||
profile,
|
||||
isBlocked: _isBlocked,
|
||||
isOwnProfile,
|
||||
repostList,
|
||||
muteLists,
|
||||
nsfwList
|
||||
@ -61,11 +60,6 @@ export const ProfilePage = () => {
|
||||
const displayName =
|
||||
profile?.displayName || profile?.name || '[name not set up]'
|
||||
const [showReportPopUp, setShowReportPopUp] = useState(false)
|
||||
const isLoggedIn = userState.auth && userState.user?.pubkey !== 'undefined'
|
||||
const isOwnProfile =
|
||||
userState.auth &&
|
||||
userState.user?.pubkey &&
|
||||
userState.user.pubkey === profilePubkey
|
||||
|
||||
const [isBlocked, setIsBlocked] = useState(_isBlocked)
|
||||
const handleBlock = async () => {
|
||||
@ -74,7 +68,7 @@ export const ProfilePage = () => {
|
||||
return
|
||||
}
|
||||
|
||||
let userHexKey: string | undefined
|
||||
let userHexKey: string
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Getting user pubkey')
|
||||
@ -82,11 +76,7 @@ export const ProfilePage = () => {
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
userHexKey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!userHexKey) {
|
||||
@ -383,25 +373,23 @@ export const ProfilePage = () => {
|
||||
</a>
|
||||
{!isOwnProfile && (
|
||||
<>
|
||||
{isLoggedIn && (
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
id='reportUser'
|
||||
onClick={() => setShowReportPopUp(true)}
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
id='reportUser'
|
||||
onClick={() => setShowReportPopUp(true)}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
||||
</svg>
|
||||
Report
|
||||
</a>
|
||||
)}
|
||||
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
||||
</svg>
|
||||
Report
|
||||
</a>
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={isBlocked ? handleUnblock : handleBlock}
|
||||
@ -518,15 +506,11 @@ const ReportUserPopup = ({
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Getting user pubkey')
|
||||
let userHexKey: string | undefined
|
||||
let userHexKey: string
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
userHexKey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!userHexKey) {
|
||||
@ -677,7 +661,7 @@ const ReportUserPopup = ({
|
||||
}
|
||||
|
||||
const ProfileTabBlogs = () => {
|
||||
const { profilePubkey, muteLists, nsfwList } =
|
||||
const { profile, muteLists, nsfwList } =
|
||||
useLoaderData() as ProfilePageLoaderResult
|
||||
const navigation = useNavigation()
|
||||
const { fetchEvents } = useNDKContext()
|
||||
@ -685,7 +669,7 @@ const ProfileTabBlogs = () => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const blogfilter: NDKFilter = useMemo(() => {
|
||||
const filter: NDKFilter = {
|
||||
authors: [profilePubkey],
|
||||
authors: [profile?.pubkey as string],
|
||||
kinds: [kinds.LongFormArticle]
|
||||
}
|
||||
|
||||
@ -699,13 +683,13 @@ const ProfileTabBlogs = () => {
|
||||
}
|
||||
|
||||
return filter
|
||||
}, [filterOptions.nsfw, filterOptions.source, profilePubkey])
|
||||
}, [filterOptions.nsfw, filterOptions.source, profile?.pubkey])
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>([])
|
||||
useEffect(() => {
|
||||
if (profilePubkey) {
|
||||
if (profile) {
|
||||
// Initial blog fetch, go beyond limit to check for next
|
||||
const filter: NDKFilter = {
|
||||
...blogfilter,
|
||||
@ -720,7 +704,7 @@ const ProfileTabBlogs = () => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
}, [blogfilter, fetchEvents, profilePubkey])
|
||||
}, [blogfilter, fetchEvents, profile])
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (isLoading) return
|
||||
@ -774,11 +758,9 @@ const ProfileTabBlogs = () => {
|
||||
let _blogs = blogs || []
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isOwner =
|
||||
userState.user?.pubkey && userState.user.pubkey === profilePubkey
|
||||
userState.user?.pubkey && userState.user.pubkey === profile?.pubkey
|
||||
const isUnmoderatedFully =
|
||||
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
|
||||
const isOnlyBlocked =
|
||||
filterOptions.moderated === ModeratedFilter.Only_Blocked
|
||||
|
||||
// Add nsfw tag to blogs included in nsfwList
|
||||
if (filterOptions.nsfw !== NSFWFilter.Hide_NSFW) {
|
||||
@ -794,16 +776,9 @@ const ProfileTabBlogs = () => {
|
||||
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
|
||||
)
|
||||
|
||||
if (isOnlyBlocked && isAdmin) {
|
||||
_blogs = _blogs.filter(
|
||||
(b) =>
|
||||
muteLists.admin.authors.includes(b.author!) ||
|
||||
muteLists.admin.replaceableEvents.includes(b.aTag!)
|
||||
)
|
||||
} else if (isUnmoderatedFully && (isAdmin || isOwner)) {
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
// Allow "Unmoderated Fully" when author visits own profile
|
||||
} else {
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
// Allow "Unmoderated Fully" when author visits own profile
|
||||
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {
|
||||
_blogs = _blogs.filter(
|
||||
(b) =>
|
||||
!muteLists.admin.authors.includes(b.author!) &&
|
||||
@ -840,7 +815,7 @@ const ProfileTabBlogs = () => {
|
||||
muteLists.user.authors,
|
||||
muteLists.user.replaceableEvents,
|
||||
nsfwList,
|
||||
profilePubkey,
|
||||
profile?.pubkey,
|
||||
userState.user?.npub,
|
||||
userState.user?.pubkey
|
||||
])
|
||||
@ -851,7 +826,10 @@ const ProfileTabBlogs = () => {
|
||||
<LoadingSpinner desc={'Loading...'} />
|
||||
)}
|
||||
|
||||
<BlogsFilter filterKey={'filter-blog'} author={profilePubkey} />
|
||||
<BlogsFilter
|
||||
filterKey={'filter-blog'}
|
||||
author={profile?.pubkey as string}
|
||||
/>
|
||||
|
||||
<div className='IBMSMList IBMSMListAlt'>
|
||||
{moderatedAndSortedBlogs.map((b) => (
|
||||
|
@ -6,7 +6,6 @@ import { store } from 'store'
|
||||
import { MuteLists, UserProfile } from 'types'
|
||||
import {
|
||||
CurationSetIdentifiers,
|
||||
getFallbackPubkey,
|
||||
getReportingSet,
|
||||
log,
|
||||
LogType,
|
||||
@ -17,6 +16,7 @@ export interface ProfilePageLoaderResult {
|
||||
profilePubkey: string
|
||||
profile: UserProfile
|
||||
isBlocked: boolean
|
||||
isOwnProfile: boolean
|
||||
muteLists: {
|
||||
admin: MuteLists
|
||||
user: MuteLists
|
||||
@ -58,17 +58,21 @@ export const profileRouteLoader =
|
||||
|
||||
// Get the current state
|
||||
const userState = store.getState().user
|
||||
const loggedInUserPubkey =
|
||||
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
|
||||
|
||||
// Check if current user is logged in
|
||||
let userPubkey: string | undefined
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
userPubkey = userState.user.pubkey as string
|
||||
}
|
||||
|
||||
// Redirect if profile naddr is missing
|
||||
// - home if user is not logged
|
||||
let profileRoute = appRoutes.home
|
||||
if (!profilePubkey && loggedInUserPubkey) {
|
||||
if (!profilePubkey && userPubkey) {
|
||||
// - own profile
|
||||
profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: loggedInUserPubkey
|
||||
pubkey: userPubkey
|
||||
})
|
||||
)
|
||||
}
|
||||
@ -79,6 +83,7 @@ export const profileRouteLoader =
|
||||
profilePubkey: profilePubkey,
|
||||
profile: {},
|
||||
isBlocked: false,
|
||||
isOwnProfile: false,
|
||||
muteLists: {
|
||||
admin: {
|
||||
authors: [],
|
||||
@ -93,9 +98,14 @@ export const profileRouteLoader =
|
||||
repostList: []
|
||||
}
|
||||
|
||||
// Check if user the user is logged in
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
result.isOwnProfile = userState.user.pubkey === profilePubkey
|
||||
}
|
||||
|
||||
const settled = await Promise.allSettled([
|
||||
ndkContext.findMetadata(profilePubkey),
|
||||
ndkContext.getMuteLists(loggedInUserPubkey),
|
||||
ndkContext.getMuteLists(userPubkey),
|
||||
getReportingSet(CurationSetIdentifiers.NSFW, ndkContext),
|
||||
getReportingSet(CurationSetIdentifiers.Repost, ndkContext)
|
||||
])
|
||||
|
@ -39,7 +39,6 @@ import {
|
||||
scrollIntoView
|
||||
} from 'utils'
|
||||
import { useCuratedSet } from 'hooks/useCuratedSet'
|
||||
import dedup from 'utils/nostr'
|
||||
|
||||
enum SearchKindEnum {
|
||||
Mods = 'Mods',
|
||||
@ -174,15 +173,12 @@ const Filters = React.memo(() => {
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(ModeratedFilter).map((item, index) => {
|
||||
const isAdmin =
|
||||
userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
if (item === ModeratedFilter.Unmoderated_Fully) {
|
||||
const isAdmin =
|
||||
userState.user?.npub ===
|
||||
import.meta.env.VITE_REPORTING_NPUB
|
||||
|
||||
if (item === ModeratedFilter.Only_Blocked && !isAdmin) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (item === ModeratedFilter.Unmoderated_Fully && !isAdmin) {
|
||||
return null
|
||||
if (!isAdmin) return null
|
||||
}
|
||||
|
||||
return (
|
||||
@ -442,16 +438,9 @@ const UsersResult = ({
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isUnmoderatedFully =
|
||||
moderationFilter === ModeratedFilter.Unmoderated_Fully
|
||||
const isOnlyBlocked = moderationFilter === ModeratedFilter.Only_Blocked
|
||||
|
||||
if (isOnlyBlocked && isAdmin) {
|
||||
filtered = filtered.filter((profile) =>
|
||||
muteLists.admin.authors.includes(profile.pubkey as string)
|
||||
)
|
||||
} else if (isUnmoderatedFully && isAdmin) {
|
||||
// Only apply filtering if the user is not an admin
|
||||
// or the admin has not selected "Unmoderated Fully"
|
||||
} else {
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
if (!(isAdmin && isUnmoderatedFully)) {
|
||||
filtered = filtered.filter(
|
||||
(profile) => !muteLists.admin.authors.includes(profile.pubkey as string)
|
||||
)
|
||||
@ -519,11 +508,6 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{searchTerm !== '' && filteredGames.length === 0 && (
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
Game not found. Send us a message where you can reach us to add it
|
||||
</div>
|
||||
)}
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
<div className='IBMSMList IBMSMListFeaturedAlt'>
|
||||
{filteredGames
|
||||
@ -537,14 +521,20 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{searchTerm !== '' && filteredGames.length > MAX_GAMES_PER_PAGE && (
|
||||
<Pagination
|
||||
page={page}
|
||||
disabledNext={filteredGames.length <= page * MAX_GAMES_PER_PAGE}
|
||||
handlePrev={handlePrev}
|
||||
handleNext={handleNext}
|
||||
/>
|
||||
)}
|
||||
<Pagination
|
||||
page={page}
|
||||
disabledNext={filteredGames.length <= page * MAX_GAMES_PER_PAGE}
|
||||
handlePrev={handlePrev}
|
||||
handleNext={handleNext}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
function dedup(event1: NDKEvent, event2: NDKEvent) {
|
||||
// return the newest of the two
|
||||
if (event1.created_at! > event2.created_at!) {
|
||||
return event1
|
||||
}
|
||||
|
||||
return event2
|
||||
}
|
||||
|
@ -55,16 +55,12 @@ export const PreferencesSetting = () => {
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
|
||||
let hexPubkey: string | undefined
|
||||
let hexPubkey: string
|
||||
|
||||
if (user?.pubkey) {
|
||||
hexPubkey = user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!hexPubkey) {
|
||||
|
@ -5,23 +5,14 @@ import { ActionFunctionArgs, redirect } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { getModPageRoute } from 'routes'
|
||||
import { store } from 'store'
|
||||
import {
|
||||
FormErrors,
|
||||
ModFormState,
|
||||
MODPERMISSIONS_CONF,
|
||||
SubmitModActionResult,
|
||||
TimeoutError
|
||||
} from 'types'
|
||||
import { FormErrors, ModFormState } from 'types'
|
||||
import {
|
||||
isReachable,
|
||||
isValidImageUrl,
|
||||
isValidUrl,
|
||||
log,
|
||||
LogType,
|
||||
MOD_DRAFT_CACHE_KEY,
|
||||
now,
|
||||
removeLocalStorageItem,
|
||||
timeout
|
||||
now
|
||||
} from 'utils'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { T_TAG_VALUE } from '../../constants'
|
||||
@ -58,31 +49,29 @@ export const submitModRouteAction =
|
||||
// Check for errors
|
||||
const formErrors = await validateState(formState)
|
||||
|
||||
// Return early if there are any errors
|
||||
if (Object.keys(formErrors).length) {
|
||||
return {
|
||||
type: 'validation',
|
||||
error: formErrors
|
||||
} as SubmitModActionResult
|
||||
}
|
||||
// Return earily if there are any errors
|
||||
if (Object.keys(formErrors).length) return formErrors
|
||||
|
||||
const currentTimeStamp = now()
|
||||
|
||||
const { naddr } = params
|
||||
// Check if we are editing or this is a new mob
|
||||
const { naddr } = params
|
||||
const isEditing = naddr && request.method === 'PUT'
|
||||
|
||||
const uuid = formState.dTag || uuidv4()
|
||||
const currentTimeStamp = now()
|
||||
const aTag =
|
||||
formState.aTag || `${kinds.ClassifiedListing}:${hexPubkey}:${uuid}`
|
||||
const published_at = formState.published_at || currentTimeStamp
|
||||
|
||||
const tags = [
|
||||
['d', uuid],
|
||||
['a', aTag],
|
||||
['r', formState.rTag],
|
||||
['t', T_TAG_VALUE],
|
||||
['published_at', published_at.toString()],
|
||||
[
|
||||
'published_at',
|
||||
isEditing
|
||||
? formState.published_at.toString()
|
||||
: currentTimeStamp.toString()
|
||||
],
|
||||
['game', formState.game],
|
||||
['title', formState.title],
|
||||
['featuredImageUrl', formState.featuredImageUrl],
|
||||
@ -96,41 +85,8 @@ export const submitModRouteAction =
|
||||
...formState.downloadUrls.map((downloadUrl) =>
|
||||
JSON.stringify(downloadUrl)
|
||||
)
|
||||
],
|
||||
[
|
||||
'otherAssets',
|
||||
formState.otherAssets?.toString() ??
|
||||
MODPERMISSIONS_CONF.otherAssets.default.toString()
|
||||
],
|
||||
[
|
||||
'uploadPermission',
|
||||
formState.uploadPermission?.toString() ??
|
||||
MODPERMISSIONS_CONF.uploadPermission.toString()
|
||||
],
|
||||
[
|
||||
'modPermission',
|
||||
formState.modPermission?.toString() ??
|
||||
MODPERMISSIONS_CONF.modPermission.toString()
|
||||
],
|
||||
[
|
||||
'convPermission',
|
||||
formState.convPermission?.toString() ??
|
||||
MODPERMISSIONS_CONF.convPermission.toString()
|
||||
],
|
||||
[
|
||||
'assetUsePermission',
|
||||
formState.assetUsePermission?.toString() ??
|
||||
MODPERMISSIONS_CONF.assetUsePermission.toString()
|
||||
],
|
||||
[
|
||||
'assetUseComPermission',
|
||||
formState.assetUseComPermission?.toString() ??
|
||||
MODPERMISSIONS_CONF.assetUseComPermission.toString()
|
||||
],
|
||||
['publisherNotes', formState.publisherNotes ?? ''],
|
||||
['extraCredits', formState.extraCredits ?? '']
|
||||
]
|
||||
]
|
||||
|
||||
if (formState.repost && formState.originalAuthor) {
|
||||
tags.push(['originalAuthor', formState.originalAuthor])
|
||||
}
|
||||
@ -173,52 +129,30 @@ export const submitModRouteAction =
|
||||
}
|
||||
|
||||
const ndkEvent = new NDKEvent(ndkContext.ndk, signedEvent)
|
||||
// Publishing a mod sometime hangs (ndk.publish has internal timeout of 10s)
|
||||
// Make sure to actually throw a timeout error (30s)
|
||||
try {
|
||||
const publishedOnRelays = await Promise.race([
|
||||
ndkContext.publish(ndkEvent),
|
||||
timeout(30000)
|
||||
])
|
||||
// Handle cases where publishing failed or succeeded
|
||||
if (publishedOnRelays.length === 0) {
|
||||
toast.error('Failed to publish event on any relay')
|
||||
} else {
|
||||
toast.success(
|
||||
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
|
||||
'\n'
|
||||
)}`
|
||||
)
|
||||
const publishedOnRelays = await ndkContext.publish(ndkEvent)
|
||||
|
||||
!isEditing && removeLocalStorageItem(MOD_DRAFT_CACHE_KEY)
|
||||
// Handle cases where publishing failed or succeeded
|
||||
if (publishedOnRelays.length === 0) {
|
||||
toast.error('Failed to publish event on any relay')
|
||||
} else {
|
||||
toast.success(
|
||||
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
|
||||
'\n'
|
||||
)}`
|
||||
)
|
||||
|
||||
const naddr = nip19.naddrEncode({
|
||||
identifier: aTag,
|
||||
pubkey: signedEvent.pubkey,
|
||||
kind: signedEvent.kind,
|
||||
relays: publishedOnRelays
|
||||
})
|
||||
const naddr = nip19.naddrEncode({
|
||||
identifier: aTag,
|
||||
pubkey: signedEvent.pubkey,
|
||||
kind: signedEvent.kind,
|
||||
relays: publishedOnRelays
|
||||
})
|
||||
|
||||
return redirect(getModPageRoute(naddr))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TimeoutError) {
|
||||
const result: SubmitModActionResult = {
|
||||
type: 'timeout',
|
||||
data: {
|
||||
dTag: uuid,
|
||||
aTag,
|
||||
published_at: published_at
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Rethrow non-timeout for general catch
|
||||
throw error
|
||||
return redirect(getModPageRoute(naddr))
|
||||
}
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, 'Failed to sign the event!', error)
|
||||
toast.error('Failed to sign the event!')
|
||||
return null
|
||||
}
|
||||
|
||||
@ -275,6 +209,13 @@ const validateState = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
formState.repost &&
|
||||
(!formState.originalAuthor || formState.originalAuthor === '')
|
||||
) {
|
||||
errors.originalAuthor = 'Original author field can not be empty'
|
||||
}
|
||||
|
||||
if (!formState.tags || formState.tags === '') {
|
||||
errors.tags = 'Tags field can not be empty'
|
||||
}
|
||||
@ -290,19 +231,6 @@ const validateState = async (
|
||||
|
||||
errors.downloadUrls![i] = 'Download url must be valid and reachable'
|
||||
}
|
||||
|
||||
if (
|
||||
downloadUrl.mediaUrl &&
|
||||
downloadUrl.mediaUrl.trim() !== '' &&
|
||||
(!isValidUrl(downloadUrl.mediaUrl) ||
|
||||
!isValidImageUrl(downloadUrl.mediaUrl) ||
|
||||
!(await isReachable(downloadUrl.mediaUrl)))
|
||||
) {
|
||||
if (!errors.downloadUrls)
|
||||
errors.downloadUrls = Array(formState.downloadUrls.length)
|
||||
|
||||
errors.downloadUrls![i] = 'Media URLs must be valid and reachable image'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LoadingSpinner, TimerLoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { ModForm } from 'components/ModForm'
|
||||
import { ProfileSection } from 'components/ProfileSection'
|
||||
import { useAppSelector } from 'hooks'
|
||||
@ -24,9 +24,7 @@ export const SubmitModPage = () => {
|
||||
<LoadingSpinner desc='Fetching mod details from relays' />
|
||||
)}
|
||||
{navigation.state === 'submitting' && (
|
||||
<TimerLoadingSpinner timeoutMs={10000} countdownMs={30000}>
|
||||
Publishing mod to relays
|
||||
</TimerLoadingSpinner>
|
||||
<LoadingSpinner desc='Publishing mod to relays' />
|
||||
)}
|
||||
<ModForm />
|
||||
</div>
|
||||
|
@ -1,3 +0,0 @@
|
||||
export const SupportersPage = () => {
|
||||
return <h2>WIP</h2>
|
||||
}
|
@ -3,13 +3,12 @@ import { ActionFunctionArgs, redirect } from 'react-router-dom'
|
||||
import { getBlogPageRoute } from 'routes'
|
||||
import { BlogFormErrors, BlogEventSubmitForm, BlogEventEditForm } from 'types'
|
||||
import {
|
||||
BLOG_DRAFT_CACHE_KEY,
|
||||
isReachable,
|
||||
isValidImageUrl,
|
||||
log,
|
||||
LogType,
|
||||
now,
|
||||
removeLocalStorageItem
|
||||
parseFormData
|
||||
} from 'utils'
|
||||
import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools'
|
||||
import { toast } from 'react-toastify'
|
||||
@ -44,9 +43,12 @@ export const writeRouteAction =
|
||||
}
|
||||
|
||||
// Get the form data from submit request
|
||||
const formSubmit = (await request.json()) as
|
||||
| BlogEventSubmitForm
|
||||
| BlogEventEditForm
|
||||
const formData = await request.formData()
|
||||
|
||||
// Parse the the data
|
||||
const formSubmit = parseFormData<BlogEventSubmitForm | BlogEventEditForm>(
|
||||
formData
|
||||
)
|
||||
|
||||
// Check for errors
|
||||
const formErrors = await validateFormData(formSubmit)
|
||||
@ -78,7 +80,7 @@ export const writeRouteAction =
|
||||
const tTags = formSubmit
|
||||
.tags!.toLowerCase()
|
||||
.split(',')
|
||||
.map((t) => ['t', t.trim()])
|
||||
.map((t) => ['t', t])
|
||||
|
||||
const tags = [
|
||||
['d', uuid],
|
||||
@ -93,7 +95,7 @@ export const writeRouteAction =
|
||||
|
||||
// Add NSFW tag, L label namespace standardized tag
|
||||
// https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags
|
||||
if (formSubmit.nsfw) tags.push(['L', 'content-warning'])
|
||||
if (formSubmit.nsfw === 'on') tags.push(['L', 'content-warning'])
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
kind: kinds.LongFormArticle,
|
||||
@ -126,9 +128,6 @@ export const writeRouteAction =
|
||||
'\n'
|
||||
)}`
|
||||
)
|
||||
|
||||
!isEditing && removeLocalStorageItem(BLOG_DRAFT_CACHE_KEY)
|
||||
|
||||
const naddr = nip19.naddrEncode({
|
||||
identifier: uuid,
|
||||
pubkey: signedEvent.pubkey,
|
||||
|
@ -1,123 +1,53 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import {
|
||||
Form,
|
||||
useActionData,
|
||||
useLoaderData,
|
||||
useNavigation,
|
||||
useSubmit
|
||||
useNavigation
|
||||
} from 'react-router-dom'
|
||||
import {
|
||||
CheckboxField,
|
||||
InputField,
|
||||
InputFieldWithImageUpload
|
||||
} from 'components/Inputs'
|
||||
import { ProfileSection } from 'components/ProfileSection'
|
||||
import { useAppSelector, useLocalCache } from 'hooks'
|
||||
import {
|
||||
BlogEventEditForm,
|
||||
BlogEventSubmitForm,
|
||||
BlogFormErrors,
|
||||
BlogPageLoaderResult
|
||||
} from 'types'
|
||||
CheckboxFieldUncontrolled,
|
||||
InputError,
|
||||
InputFieldUncontrolled
|
||||
} from '../../components/Inputs'
|
||||
import { ProfileSection } from '../../components/ProfileSection'
|
||||
import { useAppSelector } from '../../hooks'
|
||||
import { BlogFormErrors, BlogPageLoaderResult } from 'types'
|
||||
import '../../styles/innerPage.css'
|
||||
import '../../styles/styles.css'
|
||||
import '../../styles/write.css'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { AlertPopup } from 'components/AlertPopup'
|
||||
import { Editor, EditorRef } from 'components/Markdown/Editor'
|
||||
import { InputError } from 'components/Inputs/Error'
|
||||
import { BLOG_DRAFT_CACHE_KEY, initializeBlogForm } from 'utils'
|
||||
import 'styles/innerPage.css'
|
||||
import 'styles/styles.css'
|
||||
import 'styles/write.css'
|
||||
|
||||
export const WritePage = () => {
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const data = useLoaderData() as BlogPageLoaderResult
|
||||
|
||||
const formErrors = useActionData() as BlogFormErrors
|
||||
const navigation = useNavigation()
|
||||
const submit = useSubmit()
|
||||
|
||||
const blog = data?.blog
|
||||
|
||||
// Enable cache for the new blog
|
||||
const isEditing = typeof data?.blog !== 'undefined'
|
||||
const [cache, setCache, clearCache] =
|
||||
useLocalCache<BlogEventSubmitForm>(BLOG_DRAFT_CACHE_KEY)
|
||||
|
||||
const title = isEditing ? 'Edit blog post' : 'Submit a blog post'
|
||||
const [formState, setFormState] = useState<
|
||||
BlogEventSubmitForm | BlogEventEditForm
|
||||
>(isEditing ? initializeBlogForm(blog) : cache ? cache : initializeBlogForm())
|
||||
|
||||
useEffect(() => {
|
||||
!isEditing && setCache(formState)
|
||||
}, [formState, isEditing, setCache])
|
||||
|
||||
const title = data?.blog ? 'Edit blog post' : 'Submit a blog post'
|
||||
const [content, setContent] = useState(blog?.content || '')
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const editorRef = useRef<EditorRef>(null)
|
||||
|
||||
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
|
||||
const handleReset = useCallback(() => {
|
||||
const handleReset = () => {
|
||||
setShowConfirmPopup(true)
|
||||
}, [])
|
||||
const handleResetConfirm = useCallback(
|
||||
(confirm: boolean) => {
|
||||
setShowConfirmPopup(false)
|
||||
}
|
||||
const handleResetConfirm = (confirm: boolean) => {
|
||||
setShowConfirmPopup(false)
|
||||
|
||||
// Cancel if not confirmed
|
||||
if (!confirm) return
|
||||
// Cancel if not confirmed
|
||||
if (!confirm) return
|
||||
|
||||
const initialState = initializeBlogForm(blog)
|
||||
// Reset editor
|
||||
if (blog?.content) {
|
||||
editorRef.current?.setMarkdown(blog?.content)
|
||||
}
|
||||
|
||||
// Reset editor
|
||||
editorRef.current?.setMarkdown(initialState.content)
|
||||
setFormState(initialState)
|
||||
|
||||
// Clear cache
|
||||
!isEditing && clearCache()
|
||||
},
|
||||
[blog, clearCache, isEditing]
|
||||
)
|
||||
|
||||
const handleImageChange = useCallback((_name: string, value: string) => {
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
image: value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const handleInputChange = useCallback((name: string, value: string) => {
|
||||
setFormState((prevState) => ({
|
||||
...prevState,
|
||||
[name]: value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
(md: string) => {
|
||||
handleInputChange('content', md)
|
||||
},
|
||||
[handleInputChange]
|
||||
)
|
||||
|
||||
const handleCheckboxChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, checked } = e.target
|
||||
setFormState((prevState) => ({
|
||||
...prevState,
|
||||
[name]: checked
|
||||
}))
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
submit(JSON.stringify(formState), {
|
||||
method: isEditing ? 'put' : 'post',
|
||||
encType: 'application/json'
|
||||
})
|
||||
},
|
||||
[formState, isEditing, submit]
|
||||
)
|
||||
formRef.current?.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
@ -134,63 +64,80 @@ export const WritePage = () => {
|
||||
{navigation.state === 'submitting' && (
|
||||
<LoadingSpinner desc='Publishing blog to relays' />
|
||||
)}
|
||||
<form className='IBMSMSMBS_Write' onSubmit={handleFormSubmit}>
|
||||
<InputField
|
||||
<Form
|
||||
ref={formRef}
|
||||
className='IBMSMSMBS_Write'
|
||||
method={blog ? 'put' : 'post'}
|
||||
>
|
||||
<InputFieldUncontrolled
|
||||
label='Title'
|
||||
name='title'
|
||||
value={formState.title}
|
||||
defaultValue={blog?.title}
|
||||
error={formErrors?.title}
|
||||
onChange={handleInputChange}
|
||||
placeholder='Blog title'
|
||||
/>
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label className='form-label labelMain'>Content</label>
|
||||
<div className='inputMain'>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
markdown={formState.content}
|
||||
onChange={handleEditorChange}
|
||||
markdown={content}
|
||||
onChange={(md) => {
|
||||
setContent(md)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{typeof formErrors?.content !== 'undefined' && (
|
||||
<InputError message={formErrors?.content} />
|
||||
)}
|
||||
{/* encode to keep the markdown formatting */}
|
||||
<input
|
||||
name='content'
|
||||
hidden
|
||||
value={encodeURIComponent(content)}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<InputFieldWithImageUpload
|
||||
<InputFieldUncontrolled
|
||||
label='Featured Image URL'
|
||||
name='image'
|
||||
inputMode='url'
|
||||
value={formState.image}
|
||||
defaultValue={blog?.image}
|
||||
error={formErrors?.image}
|
||||
onInputChange={handleImageChange}
|
||||
placeholder='Image URL'
|
||||
/>
|
||||
<InputField
|
||||
<InputFieldUncontrolled
|
||||
label='Summary'
|
||||
name='summary'
|
||||
type='textarea'
|
||||
value={formState.summary}
|
||||
defaultValue={blog?.summary}
|
||||
error={formErrors?.summary}
|
||||
onChange={handleInputChange}
|
||||
placeholder={'This is a quick description of my blog'}
|
||||
/>
|
||||
<InputField
|
||||
<InputFieldUncontrolled
|
||||
label='Tags'
|
||||
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
|
||||
placeholder='Tags'
|
||||
name='tags'
|
||||
value={formState.tags}
|
||||
defaultValue={blog?.tTags?.join(', ')}
|
||||
error={formErrors?.tags}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<CheckboxField
|
||||
<CheckboxFieldUncontrolled
|
||||
label='This post is not safe for work (NSFW)'
|
||||
name='nsfw'
|
||||
isChecked={formState.nsfw}
|
||||
handleChange={handleCheckboxChange}
|
||||
type='stylized'
|
||||
defaultChecked={blog?.nsfw}
|
||||
/>
|
||||
|
||||
{typeof blog?.dTag !== 'undefined' && (
|
||||
<input name='dTag' hidden value={blog.dTag} readOnly />
|
||||
)}
|
||||
{typeof blog?.rTag !== 'undefined' && (
|
||||
<input name='rTag' hidden value={blog.rTag} readOnly />
|
||||
)}
|
||||
{typeof blog?.published_at !== 'undefined' && (
|
||||
<input
|
||||
name='published_at'
|
||||
hidden
|
||||
value={blog.published_at}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
<div className='IBMSMSMBS_WriteAction'>
|
||||
<button
|
||||
className='btn btnMain'
|
||||
@ -201,7 +148,7 @@ export const WritePage = () => {
|
||||
navigation.state === 'submitting'
|
||||
}
|
||||
>
|
||||
{isEditing ? 'Reset' : 'Clear fields'}
|
||||
{blog ? 'Reset' : 'Clear fields'}
|
||||
</button>
|
||||
<button
|
||||
className='btn btnMain'
|
||||
@ -222,13 +169,13 @@ export const WritePage = () => {
|
||||
handleClose={() => setShowConfirmPopup(false)}
|
||||
header={'Are you sure?'}
|
||||
label={
|
||||
isEditing
|
||||
blog
|
||||
? `Are you sure you want to clear all changes?`
|
||||
: `Are you sure you want to clear all field data?`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
{userState.auth && userState.user?.pubkey && (
|
||||
<ProfileSection pubkey={userState.user.pubkey as string} />
|
||||
|
@ -28,21 +28,17 @@ import { BlogPage } from '../pages/blog'
|
||||
import { blogRouteLoader } from '../pages/blog/loader'
|
||||
import { blogRouteAction } from '../pages/blog/action'
|
||||
import { reportRouteAction } from '../actions/report'
|
||||
import { BackupPage } from 'pages/backup'
|
||||
import { SupportersPage } from 'pages/supporters'
|
||||
import { commentsLoader } from 'loaders/comment'
|
||||
import { CommentsPopup } from 'components/comment/CommentsPopup'
|
||||
|
||||
export const appRoutes = {
|
||||
home: '/',
|
||||
games: '/games',
|
||||
game: '/game/:name',
|
||||
mods: '/mods',
|
||||
mod: '/mod/:naddr/',
|
||||
mod: '/mod/:naddr',
|
||||
modReport_actionOnly: '/mod/:naddr/report',
|
||||
about: '/about',
|
||||
blogs: '/blog',
|
||||
blog: '/blog/:naddr/',
|
||||
blog: '/blog/:naddr',
|
||||
blogEdit: '/blog/:naddr/edit',
|
||||
blogReport_actionOnly: '/blog/:naddr/report',
|
||||
submitMod: '/submit-mod',
|
||||
@ -55,9 +51,7 @@ export const appRoutes = {
|
||||
settingsAdmin: '/settings-admin',
|
||||
profile: '/profile/:nprofile?',
|
||||
feed: '/feed',
|
||||
notifications: '/notifications',
|
||||
backup: '/backup',
|
||||
supporters: '/supporters'
|
||||
notifications: '/notifications'
|
||||
}
|
||||
|
||||
export const getGamePageRoute = (name: string) =>
|
||||
@ -100,13 +94,6 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
{
|
||||
path: appRoutes.mod,
|
||||
element: <ModPage />,
|
||||
children: [
|
||||
{
|
||||
path: ':nevent',
|
||||
element: <CommentsPopup />,
|
||||
loader: commentsLoader(context)
|
||||
}
|
||||
],
|
||||
loader: modRouteLoader(context),
|
||||
action: modRouteAction(context),
|
||||
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||
@ -127,13 +114,6 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
{
|
||||
path: appRoutes.blog,
|
||||
element: <BlogPage />,
|
||||
children: [
|
||||
{
|
||||
path: ':nevent',
|
||||
element: <CommentsPopup />,
|
||||
loader: commentsLoader(context)
|
||||
}
|
||||
],
|
||||
loader: blogRouteLoader(context),
|
||||
action: blogRouteAction(context),
|
||||
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||
@ -205,14 +185,6 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: appRoutes.backup,
|
||||
element: <BackupPage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.supporters,
|
||||
element: <SupportersPage />
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <NotFoundPage />
|
||||
|
@ -1,36 +0,0 @@
|
||||
.backupList {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 15px;
|
||||
}
|
||||
|
||||
.backupListLink {
|
||||
transition: ease 0.4s;
|
||||
overflow: hidden;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
|
||||
border-radius: 10px;
|
||||
/*border: solid 1px rgba(255,255,255,0.1);*/
|
||||
position: relative;
|
||||
color: rgba(255,255,255,0.75);
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.backupListLink:hover {
|
||||
transition: ease 0.4s;
|
||||
text-decoration: unset;
|
||||
color: rgb(255,255,255);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.backupListLinkInside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 0px;
|
||||
flex-grow: 1;
|
||||
justify-content: end;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
cursor: pointer;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
padding: 5px 5px 10px 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.cardGameMainWrapperLink:hover {
|
||||
@ -41,7 +41,7 @@
|
||||
.cardGameMainTitle {
|
||||
transition: ease 0.4s;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 0 10px;
|
||||
padding: 0 10px 10px 10px;
|
||||
font-weight: bold;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
@ -59,7 +59,6 @@
|
||||
}
|
||||
|
||||
.IBMSMSMBSSCL_CBText {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSCL_CBTextStatus {
|
||||
@ -476,16 +475,7 @@ hover {
|
||||
}
|
||||
|
||||
.IBMSMSMBSSCL_CommentNoteRepliesTitle {
|
||||
width: 100%;
|
||||
margin: 0 0 15px 0;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSCL_CommentNoteRepliesTitleBtn {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSCL_CAElementLoadWrapper {
|
||||
|
@ -9,9 +9,9 @@
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 15px;
|
||||
border: solid 1px rgba(255, 255, 255, 0.05);
|
||||
border: solid 1px rgba(255,255,255,0.05);
|
||||
overflow: auto;
|
||||
max-height: 550px;
|
||||
padding: 15px;
|
||||
@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDownloadsTitle {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDownloadsElement {
|
||||
@ -36,11 +36,11 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 10px;
|
||||
border: solid 1px rgba(255, 255, 255, 0);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: solid 1px rgba(255,255,255,0);
|
||||
background: rgba(255,255,255,0.05);
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
.btnMain.IBMSMSMBSSDownloadsElementBtn {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
@ -62,8 +62,8 @@
|
||||
}
|
||||
|
||||
.btnMain.IBMSMSMBSSDownloadsElementBtn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1);
|
||||
background: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDownloadsElementInside {
|
||||
@ -71,7 +71,7 @@
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgba(255,255,255,0.5);
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
@ -97,30 +97,27 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0);
|
||||
background: rgba(255,255,255,0);
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
border: solid 1px rgba(255, 255, 255, 0.05);
|
||||
border: solid 1px rgba(255,255,255,0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDEIReactionsElement:hover {
|
||||
transition: ease 0.4s;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
border: solid 1px rgba(255, 255, 255, 0);
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: rgba(255,255,255,0.75);
|
||||
border: solid 1px rgba(255,255,255,0);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDEIReactionsElement:hover
|
||||
> .IBMSMSMBSSDEIReactionsElementIconWrapper {
|
||||
.IBMSMSMBSSDEIReactionsElement:hover > .IBMSMSMBSSDEIReactionsElementIconWrapper {
|
||||
transition: ease 0.4s;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-right: solid 1px rgba(255, 255, 255, 0);
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-right: solid 1px rgba(255,255,255,0);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDEIReactionsElement:hover
|
||||
> .IBMSMSMBSSDEIReactionsElementIconWrapper
|
||||
> .IBMSMSMBSSDEIReactionsElementIcon {
|
||||
.IBMSMSMBSSDEIReactionsElement:hover > .IBMSMSMBSSDEIReactionsElementIconWrapper > .IBMSMSMBSSDEIReactionsElementIcon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@ -150,9 +147,9 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0);
|
||||
background: rgba(255,255,255,0);
|
||||
padding: 10px 5px;
|
||||
border-right: solid 1px rgba(255, 255, 255, 0.05);
|
||||
border-right: solid 1px rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDownloadsElementInsideDetails {
|
||||
@ -163,20 +160,17 @@
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDEIReactionsElement.IBMSMSMBSSDEIReactionsElementActive {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: solid 1px rgba(255, 255, 255, 0);
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: solid 1px rgba(255,255,255,0);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDEIReactionsElement.IBMSMSMBSSDEIReactionsElementActive
|
||||
> .IBMSMSMBSSDEIReactionsElementIconWrapper {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-right: solid 1px rgba(255, 255, 255, 0);
|
||||
.IBMSMSMBSSDEIReactionsElement.IBMSMSMBSSDEIReactionsElementActive > .IBMSMSMBSSDEIReactionsElementIconWrapper {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-right: solid 1px rgba(255,255,255,0);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDEIReactionsElement.IBMSMSMBSSDEIReactionsElementActive
|
||||
> .IBMSMSMBSSDEIReactionsElementIconWrapper
|
||||
> .IBMSMSMBSSDEIReactionsElementIcon {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
.IBMSMSMBSSDEIReactionsElement.IBMSMSMBSSDEIReactionsElementActive > .IBMSMSMBSSDEIReactionsElementIconWrapper > .IBMSMSMBSSDEIReactionsElementIcon {
|
||||
color: rgba(255,255,255,0.75);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDownloadsActions {
|
||||
@ -194,7 +188,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
border: solid 1px rgba(255, 255, 255, 0.1);
|
||||
border: solid 1px rgba(255,255,255,0.1);
|
||||
overflow: auto;
|
||||
grid-gap: 1px;
|
||||
}
|
||||
@ -208,7 +202,7 @@
|
||||
|
||||
.IBMSMSMBSSDownloadsElementInsideAltTableRow:hover {
|
||||
transition: ease 0.4s;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@ -222,17 +216,12 @@
|
||||
text-align: start;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
.IBMSMSMBSSDownloadsElementInsideAltTableRowCol_Img {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDownloadsElementInsideAltTableRowCol.IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
max-width: 200px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(255,255,255,0.05);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -248,16 +237,13 @@
|
||||
transition: ease 0.4s;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
color: rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDownloadsElementInsideAltText:hover {
|
||||
transition: ease 0.4s;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
color: rgba(255,255,255,0.75);
|
||||
}
|
||||
|
||||
.IBMSMSMBSSDownloadsElementDtitle {
|
||||
color: rgb(255,255,255,0.5);
|
||||
}
|
||||
|
@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #666;
|
||||
background-color: mediumpurple;
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
@ -92,8 +92,6 @@
|
||||
img {
|
||||
background: #232323;
|
||||
border-radius: 10px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.editor {
|
||||
@ -102,6 +100,10 @@
|
||||
min-height: 75px;
|
||||
}
|
||||
.viewer {
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
@ -142,11 +144,7 @@
|
||||
}
|
||||
}
|
||||
.mdxeditor {
|
||||
--baseBg: #262626;
|
||||
}
|
||||
.mdxeditor-toolbar {
|
||||
top: 10px;
|
||||
border: 1px solid rgb(255, 255, 255, 0.1);
|
||||
--baseBg: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.mdxeditor,
|
||||
.mdxeditor-popup-container {
|
||||
|
@ -283,132 +283,9 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSNote {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
border: solid 2px rgba(255,255,255,0.1);
|
||||
background: rgba(255,122,0,0.2);
|
||||
color: rgba(255,255,255,0.95);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSPostBodyHideText > * {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtra {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 0px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.1);
|
||||
border: solid 1px rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.btnMain.IBMSMSMBSSExtraBtn {
|
||||
border-radius: 0px;
|
||||
flex-direction: column;
|
||||
grid-gap: 0px;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBox {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElement {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 0px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementCol {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
color: rgba(255,255,255,0.5);
|
||||
padding: 10px;
|
||||
grid-gap: 5px;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementCol.IBMSMSMBSSExtraBoxElementColStart {
|
||||
font-weight: bold;
|
||||
background: rgba(255,255,255,0.05);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementCol.IBMSMSMBSSExtraBoxElementColSecond {
|
||||
flex-grow: 1;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementWrapper {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementColMark {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementColMark.IBMSMSMBSSExtraBoxElementColMarkGreen {
|
||||
color: #6cff6c;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementColMark.IBMSMSMBSSExtraBoxElementColMarkRed {
|
||||
color: tomato;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementColChoice {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
grid-gap: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementColChoiceRadio {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementColChoiceBox {
|
||||
transition: ease 0.2s;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
border: solid 2px rgba(255,255,255,0);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.IBMSMSMBSSExtraBoxElementColChoiceRadio:checked + .IBMSMSMBSSExtraBoxElementColChoiceBox {
|
||||
border: solid 2px rgba(255,255,255,0.5);
|
||||
}
|
@ -661,7 +661,6 @@ a:hover {
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.successMain,
|
||||
.errorMain {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
@ -672,17 +671,13 @@ a:hover {
|
||||
flex-direction: row;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
.successMainColor,
|
||||
|
||||
.errorMainColor {
|
||||
width: 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.errorMainColor {
|
||||
background: tomato;
|
||||
}
|
||||
.successMainColor {
|
||||
background: #60ae60;
|
||||
}
|
||||
|
||||
.errorMainText {
|
||||
}
|
||||
|
||||
@ -710,39 +705,3 @@ a:hover {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.uploadBoxMain {
|
||||
background: hsl(0deg 0% 0% / 10%);
|
||||
border-radius: 10px;
|
||||
height: 150px;
|
||||
padding: 10px;
|
||||
border: solid 1px hsl(0deg 0% 100% / 5%);
|
||||
transition: padding ease-in-out 0.4s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.uploadBoxMain:hover {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.uploadBoxMainInside {
|
||||
padding: 10px;
|
||||
border: dashed 2px hsl(0deg 0% 100% / 5%);
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: hsl(0deg 0% 100% / 20%);
|
||||
grid-gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inputLabelWrapperMain.inputLabelWrapperMainQR {
|
||||
align-items: center;
|
||||
padding: 25px;
|
||||
background: rgb(0 0 0 / 10%);
|
||||
border-radius: 10px;
|
||||
border: solid 1px rgb(255 255 255 / 10%);
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { SortBy, NSFWFilter, ModeratedFilter } from './modsFilter'
|
||||
|
||||
export interface BlogForm {
|
||||
@ -21,7 +20,9 @@ export interface BlogDetails extends BlogForm {
|
||||
tTags: string[]
|
||||
}
|
||||
|
||||
export interface BlogEventSubmitForm extends BlogForm {}
|
||||
export interface BlogEventSubmitForm extends Omit<BlogForm, 'nsfw'> {
|
||||
nsfw: string
|
||||
}
|
||||
|
||||
export interface BlogEventEditForm extends BlogEventSubmitForm {
|
||||
dTag: string
|
||||
@ -37,11 +38,9 @@ export interface BlogCardDetails extends BlogDetails {
|
||||
|
||||
export interface BlogPageLoaderResult {
|
||||
blog: Partial<BlogDetails> | undefined
|
||||
event: NDKEvent | undefined
|
||||
latest: Partial<BlogDetails>[]
|
||||
isAddedToNSFW: boolean
|
||||
isBlocked: boolean
|
||||
postWarning?: 'user' | 'admin'
|
||||
}
|
||||
|
||||
export interface BlogsFilterOptions {
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
export interface CommentsLoaderResult {
|
||||
event: NDKEvent
|
||||
}
|
||||
|
||||
export enum SortByEnum {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest'
|
||||
}
|
||||
|
||||
export enum AuthorFilterEnum {
|
||||
All_Comments = 'All Comments',
|
||||
Creator_Comments = 'Creator Comments'
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
import { toast } from 'react-toastify'
|
||||
import { log, LogType } from 'utils'
|
||||
|
||||
export type Jsonable =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
| readonly Jsonable[]
|
||||
| { readonly [key: string]: Jsonable }
|
||||
| { toJSON(): Jsonable }
|
||||
|
||||
/**
|
||||
* Handle errors
|
||||
* Wraps the errors without message property and stringify to a message so we can use it later
|
||||
* @param error
|
||||
* @returns
|
||||
*/
|
||||
export function handleError(error: unknown): Error {
|
||||
if (error instanceof Error) return error
|
||||
|
||||
// No message error, wrap it and stringify
|
||||
let stringified = 'Unable to stringify the thrown value'
|
||||
try {
|
||||
stringified = JSON.stringify(error)
|
||||
} catch (error) {
|
||||
console.error(stringified, error)
|
||||
}
|
||||
|
||||
return new Error(`Stringified Error: ${stringified}`)
|
||||
}
|
||||
|
||||
export class BaseError extends Error {
|
||||
public readonly context?: Jsonable
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
options: { cause?: Error; context?: Jsonable } = {}
|
||||
) {
|
||||
const { cause, context } = options
|
||||
|
||||
super(message, { cause: cause })
|
||||
this.name = this.constructor.name
|
||||
this.context = context
|
||||
Object.setPrototypeOf(this, BaseError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export function errorFeedback(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
log(true, LogType.Error, error)
|
||||
} else {
|
||||
toast.error('Something went wrong.')
|
||||
log(true, LogType.Error, error)
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeoutError extends Error {
|
||||
constructor(timeoutMs?: number) {
|
||||
let message = 'Time elapsed.'
|
||||
if (timeoutMs) {
|
||||
message += `\n* ${timeoutMs}ms`
|
||||
}
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
Object.setPrototypeOf(this, TimeoutError.prototype)
|
||||
}
|
||||
}
|
@ -6,5 +6,3 @@ export * from './zap'
|
||||
export * from './blog'
|
||||
export * from './category'
|
||||
export * from './popup'
|
||||
export * from './errors'
|
||||
export * from './comments'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { BlogDetails } from 'types'
|
||||
|
||||
export enum CommentEventStatus {
|
||||
@ -7,8 +7,7 @@ export enum CommentEventStatus {
|
||||
Failed = 'Failed to publish comment.'
|
||||
}
|
||||
|
||||
export interface CommentEvent {
|
||||
event: NDKEvent
|
||||
export interface CommentEvent extends Event {
|
||||
status?: CommentEventStatus
|
||||
}
|
||||
|
||||
@ -38,38 +37,15 @@ export interface ModFormState {
|
||||
lTags: string[]
|
||||
downloadUrls: DownloadUrl[]
|
||||
published_at: number
|
||||
|
||||
// Permissions and details
|
||||
otherAssets?: boolean
|
||||
uploadPermission?: boolean
|
||||
modPermission?: boolean
|
||||
convPermission?: boolean
|
||||
assetUsePermission?: boolean
|
||||
assetUseComPermission?: boolean
|
||||
publisherNotes?: string
|
||||
extraCredits?: string
|
||||
}
|
||||
|
||||
// Permissions
|
||||
export type ModPermissions = Pick<
|
||||
ModFormState,
|
||||
| 'otherAssets'
|
||||
| 'uploadPermission'
|
||||
| 'modPermission'
|
||||
| 'convPermission'
|
||||
| 'assetUsePermission'
|
||||
| 'assetUseComPermission'
|
||||
>
|
||||
|
||||
export interface DownloadUrl {
|
||||
url: string
|
||||
title?: string
|
||||
hash: string
|
||||
signatureKey: string
|
||||
malwareScanLink: string
|
||||
modVersion: string
|
||||
customNote: string
|
||||
mediaUrl?: string
|
||||
}
|
||||
|
||||
export interface ModDetails extends Omit<ModFormState, 'tags'> {
|
||||
@ -86,25 +62,12 @@ export interface MuteLists {
|
||||
|
||||
export interface ModPageLoaderResult {
|
||||
mod: ModDetails | undefined
|
||||
event: NDKEvent | undefined
|
||||
latest: Partial<BlogDetails>[]
|
||||
isAddedToNSFW: boolean
|
||||
isBlocked: boolean
|
||||
isRepost: boolean
|
||||
postWarning?: 'user' | 'admin'
|
||||
}
|
||||
|
||||
export type SubmitModActionResult =
|
||||
| { type: 'validation'; error: FormErrors }
|
||||
| {
|
||||
type: 'timeout'
|
||||
data: {
|
||||
dTag: string
|
||||
aTag: string
|
||||
published_at: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface FormErrors {
|
||||
game?: string
|
||||
title?: string
|
||||
@ -118,61 +81,3 @@ export interface FormErrors {
|
||||
author?: string
|
||||
originalAuthor?: string
|
||||
}
|
||||
|
||||
type ModPermissionsKeys = keyof ModPermissions
|
||||
type ModPermissionsOpts = 'true' | 'false'
|
||||
type ModPermissionsKeysOpts = `${ModPermissionsKeys}_${ModPermissionsOpts}`
|
||||
type ModPermissionsDesc = {
|
||||
[k in ModPermissionsKeysOpts]: string
|
||||
}
|
||||
type ModPermissionsConf = {
|
||||
[k in ModPermissionsKeys]: {
|
||||
header: string
|
||||
default: boolean
|
||||
}
|
||||
}
|
||||
export const MODPERMISSIONS_CONF: ModPermissionsConf = {
|
||||
otherAssets: {
|
||||
header: `Others' Assets`,
|
||||
default: true
|
||||
},
|
||||
uploadPermission: {
|
||||
header: `Redistribution Permission`,
|
||||
default: true
|
||||
},
|
||||
modPermission: {
|
||||
header: `Modification Permission`,
|
||||
default: true
|
||||
},
|
||||
convPermission: {
|
||||
header: `Conversion Permission`,
|
||||
default: true
|
||||
},
|
||||
assetUsePermission: {
|
||||
header: `Asset Use Permission`,
|
||||
default: true
|
||||
},
|
||||
assetUseComPermission: {
|
||||
header: `Asset Use Permission for Commercial Mods`,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
export const MODPERMISSIONS_DESC: ModPermissionsDesc = {
|
||||
otherAssets_true: `All assets in this file are either owned by the publisher or sourced from free-to-use modder's resources.`,
|
||||
otherAssets_false: `Not all assets in this file are owned by the publisher or sourced from free-to-use modder's resources; some assets may be.`,
|
||||
|
||||
uploadPermission_true: `You are allowed to upload this file to other sites, but you must give credit to me as the creator (unless indicated otherwise).`,
|
||||
uploadPermission_false: `You are not allowed to upload this file to other sites without explicit permission.`,
|
||||
|
||||
modPermission_true: `You may modify my files and release bug fixes or enhancements, provided that you credit me as the original creator.`,
|
||||
modPermission_false: `You are not allowed to convert this file for use with other games without explicit permission.`,
|
||||
|
||||
convPermission_true: `You are permitted to convert this file for use with other games, as long as you credit me as the creator.`,
|
||||
convPermission_false: `You are not permitted to convert this file for use with other games without explicit permission.`,
|
||||
|
||||
assetUsePermission_true: `You may use the assets in this file without needing permission, provided you give me credit.`,
|
||||
assetUsePermission_false: `You must obtain explicit permission to use the assets in this file.`,
|
||||
|
||||
assetUseComPermission_true: `You are allowed to use assets from this file in mods or files that are sold for money on Steam Workshop or other platforms.`,
|
||||
assetUseComPermission_false: `You are prohibited from using assets from this file in any mods or files that are sold for money on Steam Workshop or other platforms, unless given explicit permission.`
|
||||
}
|
||||
|
@ -14,8 +14,7 @@ export enum NSFWFilter {
|
||||
export enum ModeratedFilter {
|
||||
Moderated = 'Moderated',
|
||||
Unmoderated = 'Unmoderated',
|
||||
Unmoderated_Fully = 'Unmoderated Fully',
|
||||
Only_Blocked = 'Only Moderated'
|
||||
Unmoderated_Fully = 'Unmoderated Fully'
|
||||
}
|
||||
|
||||
export enum WOTFilterOptions {
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import {
|
||||
BlogCardDetails,
|
||||
BlogDetails,
|
||||
BlogEventEditForm,
|
||||
BlogEventSubmitForm
|
||||
} from 'types'
|
||||
import { BlogCardDetails, BlogDetails } from 'types'
|
||||
import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
|
||||
import { kinds, nip19 } from 'nostr-tools'
|
||||
|
||||
@ -55,25 +50,3 @@ export const extractBlogCardDetails = (
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const initializeBlogForm = (
|
||||
blog?: Partial<BlogDetails>
|
||||
): BlogEventSubmitForm | BlogEventEditForm => ({
|
||||
content: blog?.content || '',
|
||||
image: blog?.image || '',
|
||||
nsfw: blog?.nsfw || false,
|
||||
summary: blog?.summary || '',
|
||||
title: blog?.title || '',
|
||||
tags: blog?.tTags?.join(', ') || '',
|
||||
...(blog?.aTag && {
|
||||
aTag: blog.aTag
|
||||
}),
|
||||
...(blog?.dTag && {
|
||||
dTag: blog.dTag
|
||||
}),
|
||||
...(blog?.published_at && {
|
||||
published_at: blog.published_at
|
||||
})
|
||||
})
|
||||
|
||||
export const BLOG_DRAFT_CACHE_KEY = 'draft-blog'
|
||||
|
@ -1,106 +0,0 @@
|
||||
import NDK, { NDKEvent, NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||
import { toast } from 'react-toastify'
|
||||
import { CommentEvent, CommentEventStatus } from 'types'
|
||||
import { log, LogType } from './utils'
|
||||
|
||||
export function handleCommentSubmit(
|
||||
event: NDKEvent | undefined,
|
||||
setCommentEvents: React.Dispatch<React.SetStateAction<CommentEvent[]>>,
|
||||
setVisible: React.Dispatch<React.SetStateAction<CommentEvent[]>>,
|
||||
ndk: NDK
|
||||
) {
|
||||
return async (content: string): Promise<boolean> => {
|
||||
if (content === '') return false
|
||||
|
||||
// NDKEvent required
|
||||
if (!event) return false
|
||||
|
||||
let id: string | undefined
|
||||
try {
|
||||
const reply = event.reply()
|
||||
reply.content = content.trim()
|
||||
|
||||
setCommentEvents((prev) => {
|
||||
const newCommentEvents = [
|
||||
{
|
||||
event: reply,
|
||||
status: CommentEventStatus.Publishing
|
||||
},
|
||||
...prev
|
||||
]
|
||||
setVisible(newCommentEvents)
|
||||
return newCommentEvents
|
||||
})
|
||||
|
||||
if (!ndk.signer) {
|
||||
ndk.signer = new NDKNip07Signer()
|
||||
}
|
||||
await reply.sign(ndk.signer)
|
||||
id = reply.id
|
||||
const relaySet = await reply.publish()
|
||||
if (relaySet.size) {
|
||||
setCommentEvents((prev) => {
|
||||
const newCommentEvents = prev.map((ce) => {
|
||||
if (ce.event.id === reply.id) {
|
||||
return {
|
||||
event: ce.event,
|
||||
status: CommentEventStatus.Published
|
||||
}
|
||||
}
|
||||
return ce
|
||||
})
|
||||
setVisible(newCommentEvents)
|
||||
return newCommentEvents
|
||||
})
|
||||
// when an event is successfully published remove the status from it after 15 seconds
|
||||
setTimeout(() => {
|
||||
setCommentEvents((prev) => {
|
||||
const newCommentEvents = prev.map((ce) => {
|
||||
if (ce.event.id === reply.id) {
|
||||
delete ce.status
|
||||
}
|
||||
|
||||
return ce
|
||||
})
|
||||
setVisible(newCommentEvents)
|
||||
return newCommentEvents
|
||||
})
|
||||
}, 15000)
|
||||
return true
|
||||
} else {
|
||||
log(true, LogType.Error, 'Publishing reply failed.')
|
||||
setCommentEvents((prev) => {
|
||||
const newCommentEvents = prev.map((ce) => {
|
||||
if (ce.event.id === reply.id) {
|
||||
return {
|
||||
event: ce.event,
|
||||
status: CommentEventStatus.Failed
|
||||
}
|
||||
}
|
||||
return ce
|
||||
})
|
||||
setVisible(newCommentEvents)
|
||||
return newCommentEvents
|
||||
})
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('An error occurred in publishing reply.')
|
||||
log(true, LogType.Error, 'An error occurred in publishing reply.', error)
|
||||
setCommentEvents((prev) => {
|
||||
const newCommentEvents = prev.map((ce) => {
|
||||
if (ce.event.id === id) {
|
||||
return {
|
||||
event: ce.event,
|
||||
status: CommentEventStatus.Failed
|
||||
}
|
||||
}
|
||||
return ce
|
||||
})
|
||||
setVisible(newCommentEvents)
|
||||
return newCommentEvents
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -9,4 +9,3 @@ export * from './consts'
|
||||
export * from './blog'
|
||||
export * from './curationSets'
|
||||
export * from './category'
|
||||
export * from './comments'
|
||||
|
@ -1,8 +1,7 @@
|
||||
import _ from 'lodash'
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { ModDetails, ModFormState } from '../types'
|
||||
import { getTagValue, getTagValues, getFirstTagValue as _getFirstTagValue } from './nostr'
|
||||
import { getTagValue, getTagValues } from './nostr'
|
||||
|
||||
/**
|
||||
* Extracts and normalizes mod data from an event.
|
||||
@ -26,14 +25,6 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
|
||||
return tagValue ? parseInt(tagValue[0], 10) : defaultValue
|
||||
}
|
||||
|
||||
// Optional mod values
|
||||
const otherAssets = _getFirstTagValue(event, 'otherAssets')
|
||||
const uploadPermission = _getFirstTagValue(event, 'uploadPermission')
|
||||
const modPermission = _getFirstTagValue(event, 'modPermission')
|
||||
const convPermission = _getFirstTagValue(event, 'convPermission')
|
||||
const assetUsePermission = _getFirstTagValue(event, 'assetUsePermission')
|
||||
const assetUseComPermission = _getFirstTagValue(event, 'assetUseComPermission')
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
dTag: getFirstTagValue('d'),
|
||||
@ -60,15 +51,7 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
|
||||
),
|
||||
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
|
||||
JSON.parse(item)
|
||||
),
|
||||
otherAssets: otherAssets ? otherAssets === 'true' : undefined,
|
||||
uploadPermission: uploadPermission ? uploadPermission === 'true' : undefined,
|
||||
modPermission: modPermission ? modPermission === 'true' : undefined,
|
||||
convPermission: convPermission ? convPermission === 'true' : undefined,
|
||||
assetUsePermission: assetUsePermission ? assetUsePermission === 'true' : undefined,
|
||||
assetUseComPermission: assetUseComPermission ? assetUseComPermission === 'true' : undefined,
|
||||
publisherNotes: _getFirstTagValue(event, 'publisherNotes'),
|
||||
extraCredits: _getFirstTagValue(event, 'extraCredits')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,30 +131,16 @@ export const initializeFormState = (
|
||||
originalAuthor: existingModData?.originalAuthor || undefined,
|
||||
screenshotsUrls: existingModData?.screenshotsUrls || [''],
|
||||
tags: existingModData?.tags.join(',') || '',
|
||||
lTags: existingModData ? _.clone(existingModData.lTags) : [],
|
||||
LTags: existingModData ? _.clone(existingModData.lTags) : [],
|
||||
downloadUrls: existingModData
|
||||
? _.cloneDeep(existingModData.downloadUrls)
|
||||
: [
|
||||
{
|
||||
title: '',
|
||||
url: '',
|
||||
hash: '',
|
||||
signatureKey: '',
|
||||
malwareScanLink: '',
|
||||
modVersion: '',
|
||||
customNote: '',
|
||||
mediaUrl: ''
|
||||
}
|
||||
],
|
||||
otherAssets: existingModData?.otherAssets ?? true,
|
||||
uploadPermission: existingModData?.uploadPermission ?? true,
|
||||
modPermission: existingModData?.modPermission ?? true,
|
||||
convPermission:existingModData?.convPermission ?? true,
|
||||
assetUsePermission:existingModData?.assetUsePermission ?? true,
|
||||
assetUseComPermission:existingModData?.assetUseComPermission ?? false,
|
||||
publisherNotes:existingModData?.publisherNotes || '',
|
||||
extraCredits:existingModData?.extraCredits || ''
|
||||
lTags: existingModData?.lTags || [],
|
||||
LTags: existingModData?.LTags || [],
|
||||
downloadUrls: existingModData?.downloadUrls || [
|
||||
{
|
||||
url: '',
|
||||
hash: '',
|
||||
signatureKey: '',
|
||||
malwareScanLink: '',
|
||||
modVersion: '',
|
||||
customNote: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export const MOD_DRAFT_CACHE_KEY = 'draft-mod'
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user