Merge pull request #3 from reyamir/feat/note-processor

Implemented note processor
This commit is contained in:
Ren Amamiya 2023-03-03 14:22:50 +07:00 committed by GitHub
commit 4d07ff94fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1056 additions and 1524 deletions

View File

@ -8,16 +8,7 @@
"endOfLine": "lf",
"bracketSpacing": true,
"bracketSameLine": true,
"importOrder": [
"^@layouts/(.*)$",
"^@pages/(.*)$",
"^@components/(.*)$",
"^@utils/(.*)$",
"^@stores/(.*)$",
"^@assets/(.*)$",
"<THIRD_PARTY_MODULES>",
"^[./]"
],
"importOrder": ["^@layouts/(.*)$", "^@pages/(.*)$", "^@components/(.*)$", "^@utils/(.*)$", "^@stores/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],

View File

@ -9,6 +9,9 @@ module.exports = removeImports({
typescript: {
ignoreBuildErrors: true,
},
experimental: {
scrollRestoration: true,
},
webpack: (config) => {
config.experiments = { ...config.experiments, topLevelAwait: true };
return config;

View File

@ -12,11 +12,10 @@
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@nanostores/persistent": "^0.7.0",
"@nanostores/react": "^0.4.1",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.3",
"@radix-ui/react-icons": "^1.2.0",
"@rehooks/local-storage": "^2.4.4",
"@tauri-apps/api": "^1.2.0",
"@uiw/react-markdown-preview": "^4.1.9",
"@uiw/react-md-editor": "^3.20.5",
@ -27,8 +26,8 @@
"nanostores": "^0.7.4",
"next": "^13.2.1",
"next-remove-imports": "^1.0.10",
"nostr-react": "^0.6.4",
"nostr-tools": "^1.6.0",
"nostr-relaypool": "^0.5.3",
"nostr-tools": "^1.7.1",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -37,25 +36,26 @@
"react-player": "^2.11.2",
"react-virtuoso": "^4.1.0",
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
"unique-names-generator": "^4.7.1"
"unique-names-generator": "^4.7.1",
"ws": "^8.12.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "^1.2.3",
"@trivago/prettier-plugin-sort-imports": "^4.1.0",
"@types/node": "^18.14.1",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/node": "^18.14.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"autoprefixer": "^10.4.13",
"csstype": "^3.1.1",
"eslint": "^8.34.0",
"eslint": "^8.35.0",
"eslint-config-next": "^13.2.1",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.0",
"husky": "^8.0.3",
"lint-staged": "^13.1.2",
"postcss": "^8.4.21",
"prettier": "^2.8.4",

View File

@ -1,40 +1,39 @@
lockfileVersion: 5.4
specifiers:
'@nanostores/persistent': ^0.7.0
'@nanostores/react': ^0.4.1
'@radix-ui/react-dialog': ^1.0.2
'@radix-ui/react-dropdown-menu': ^2.0.3
'@radix-ui/react-icons': ^1.2.0
'@rehooks/local-storage': ^2.4.4
'@tailwindcss/typography': ^0.5.9
'@tauri-apps/api': ^1.2.0
'@tauri-apps/cli': ^1.2.3
'@trivago/prettier-plugin-sort-imports': ^4.1.0
'@types/node': ^18.14.1
'@trivago/prettier-plugin-sort-imports': ^4.1.1
'@types/node': ^18.14.2
'@types/react': ^18.0.28
'@types/react-dom': ^18.0.11
'@typescript-eslint/eslint-plugin': ^5.53.0
'@typescript-eslint/parser': ^5.53.0
'@typescript-eslint/eslint-plugin': ^5.54.0
'@typescript-eslint/parser': ^5.54.0
'@uiw/react-markdown-preview': ^4.1.9
'@uiw/react-md-editor': ^3.20.5
autoprefixer: ^10.4.13
bitcoin-address-validation: ^2.2.1
boring-avatars: ^1.7.0
csstype: ^3.1.1
eslint: ^8.34.0
eslint: ^8.35.0
eslint-config-next: ^13.2.1
eslint-config-prettier: ^8.6.0
eslint-plugin-react: ^7.32.2
eslint-plugin-react-hooks: ^4.6.0
framer-motion: ^9.1.7
husky: ^8.0.0
husky: ^8.0.3
lint-staged: ^13.1.2
moment: ^2.29.4
nanostores: ^0.7.4
next: ^13.2.1
next-remove-imports: ^1.0.10
nostr-react: ^0.6.4
nostr-tools: ^1.6.0
nostr-relaypool: ^0.5.3
nostr-tools: ^1.7.1
postcss: ^8.4.21
prettier: ^2.8.4
prettier-plugin-tailwindcss: ^0.2.3
@ -50,13 +49,13 @@ specifiers:
tauri-plugin-sql-api: github:tauri-apps/tauri-plugin-sql
typescript: ^4.9.5
unique-names-generator: ^4.7.1
ws: ^8.12.1
dependencies:
'@nanostores/persistent': 0.7.0_nanostores@0.7.4
'@nanostores/react': 0.4.1_nkfnbc2tpc77iht7asm3uqwau4
'@radix-ui/react-dialog': 1.0.2_zula6vjvt3wdocc4mwcxqa6nzi
'@radix-ui/react-dropdown-menu': 2.0.3_zula6vjvt3wdocc4mwcxqa6nzi
'@radix-ui/react-icons': 1.2.0_react@18.2.0
'@rehooks/local-storage': 2.4.4_react@18.2.0
'@tauri-apps/api': 1.2.0
'@uiw/react-markdown-preview': 4.1.9_zula6vjvt3wdocc4mwcxqa6nzi
'@uiw/react-md-editor': 3.20.5_zula6vjvt3wdocc4mwcxqa6nzi
@ -67,8 +66,8 @@ dependencies:
nanostores: 0.7.4
next: 13.2.1_biqbaboplfbrettd7655fr4n2y
next-remove-imports: 1.0.10
nostr-react: 0.6.4_react@18.2.0
nostr-tools: 1.6.0
nostr-relaypool: 0.5.3_ws@8.12.1
nostr-tools: 1.7.1
qrcode.react: 3.1.0_react@18.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
@ -78,28 +77,29 @@ dependencies:
react-virtuoso: 4.1.0_biqbaboplfbrettd7655fr4n2y
tauri-plugin-sql-api: github.com/tauri-apps/tauri-plugin-sql/abd8759ef49e1ba441540a2260b453d43d86c7ee
unique-names-generator: 4.7.1
ws: 8.12.1
devDependencies:
'@tailwindcss/typography': 0.5.9_tailwindcss@3.2.7
'@tauri-apps/cli': 1.2.3
'@trivago/prettier-plugin-sort-imports': 4.1.0_prettier@2.8.4
'@types/node': 18.14.1
'@trivago/prettier-plugin-sort-imports': 4.1.1_prettier@2.8.4
'@types/node': 18.14.2
'@types/react': 18.0.28
'@types/react-dom': 18.0.11
'@typescript-eslint/eslint-plugin': 5.53.0_ny4s7qc6yg74faf3d6xty2ofzy
'@typescript-eslint/parser': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
autoprefixer: 10.4.13_postcss@8.4.21
csstype: 3.1.1
eslint: 8.34.0
eslint-config-next: 13.2.1_7kw3g6rralp5ps6mg3uyzz6azm
eslint-config-prettier: 8.6.0_eslint@8.34.0
eslint-plugin-react: 7.32.2_eslint@8.34.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.34.0
eslint: 8.35.0
eslint-config-next: 13.2.1_ycpbpc6yetojsgtrx3mwntkhsu
eslint-config-prettier: 8.6.0_eslint@8.35.0
eslint-plugin-react: 7.32.2_eslint@8.35.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.35.0
husky: 8.0.3
lint-staged: 13.1.2
postcss: 8.4.21
prettier: 2.8.4
prettier-plugin-tailwindcss: 0.2.3_3p4xqifn6m4d44r76wgcnqfi3i
prettier-plugin-tailwindcss: 0.2.3_zmkqdpv3ldc45e6wei6qtrbrca
prop-types: 15.8.1
tailwindcss: 3.2.7_postcss@8.4.21
typescript: 4.9.5
@ -350,8 +350,8 @@ packages:
dev: false
optional: true
/@eslint/eslintrc/1.4.1:
resolution: { integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== }
/@eslint/eslintrc/2.0.0:
resolution: { integrity: sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dependencies:
ajv: 6.12.6
@ -367,6 +367,11 @@ packages:
- supports-color
dev: true
/@eslint/js/8.35.0:
resolution: { integrity: sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dev: true
/@floating-ui/core/0.7.3:
resolution: { integrity: sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg== }
dev: false
@ -411,6 +416,15 @@ packages:
resolution: { integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== }
dev: true
/@jest/source-map/29.4.3:
resolution: { integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w== }
engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 }
dependencies:
'@jridgewell/trace-mapping': 0.3.17
callsites: 3.1.0
graceful-fs: 4.2.10
dev: false
/@jridgewell/gen-mapping/0.1.1:
resolution: { integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== }
engines: { node: '>=6.0.0' }
@ -449,27 +463,6 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: false
/@nanostores/persistent/0.7.0_nanostores@0.7.4:
resolution: { integrity: sha512-4PAInL/T1hbftZUJ0cmgdFHBMalUoq7BUXFBy7QfyMv/8X3LPTYNh/yxspL7+J+XM3UNvVI7IFRMMs6FBasjhQ== }
engines: { node: ^14.0.0 || ^16.0.0 || >=18.0.0 }
peerDependencies:
nanostores: ^0.7.0
dependencies:
nanostores: 0.7.4
dev: false
/@nanostores/react/0.4.1_nkfnbc2tpc77iht7asm3uqwau4:
resolution: { integrity: sha512-lsv0CYrMxczbXtoV/mxFVEoL/uVjEjseoP89srO/5yNAOkJka+dSFS7LYyWEbuvCPO7EgbtkvRpO5V+OztKQOw== }
engines: { node: ^14.0.0 || ^16.0.0 || >=18.0.0 }
peerDependencies:
nanostores: ^0.7.0
react: '>=18.0.0'
dependencies:
nanostores: 0.7.4
react: 18.2.0
use-sync-external-store: 1.2.0_react@18.2.0
dev: false
/@next/env/13.2.1:
resolution: { integrity: sha512-Hq+6QZ6kgmloCg8Kgrix+4F0HtvLqVK3FZAnlAoS0eonaDemHe1Km4kwjSWRE3JNpJNcKxFHF+jsZrYo0SxWoQ== }
dev: false
@ -991,6 +984,14 @@ packages:
'@babel/runtime': 7.21.0
dev: false
/@rehooks/local-storage/2.4.4_react@18.2.0:
resolution: { integrity: sha512-zE+kfOkG59n/1UTxdmbwktIosclr67Nlbf2MzUJ9mNtCSypVscNHeD1qT6JCSo5Pjj8DO893IKWNLJqKKzDL/Q== }
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.2.0
dev: false
/@rushstack/eslint-patch/1.2.0:
resolution: { integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== }
dev: true
@ -1134,8 +1135,8 @@ packages:
'@tauri-apps/cli-win32-x64-msvc': 1.2.3
dev: true
/@trivago/prettier-plugin-sort-imports/4.1.0_prettier@2.8.4:
resolution: { integrity: sha512-aTr6QPFaPAAzPRFn9yWB/9yKi3ZAFqfGpxIGLPWuQfYJFGUed+W3KKwxntsoCiNvNE2iuKOg6haMo5KG8WXltg== }
/@trivago/prettier-plugin-sort-imports/4.1.1_prettier@2.8.4:
resolution: { integrity: sha512-dQ2r2uzNr1x6pJsuh/8x0IRA3CBUB+pWEW3J/7N98axqt7SQSm+2fy0FLNXvXGg77xEDC7KHxJlHfLYyi7PDcw== }
peerDependencies:
'@vue/compiler-sfc': 3.x
prettier: 2.x
@ -1183,8 +1184,8 @@ packages:
resolution: { integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== }
dev: false
/@types/node/18.14.1:
resolution: { integrity: sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ== }
/@types/node/18.14.2:
resolution: { integrity: sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA== }
dev: true
/@types/parse5/6.0.3:
@ -1222,8 +1223,8 @@ packages:
resolution: { integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== }
dev: false
/@typescript-eslint/eslint-plugin/5.53.0_ny4s7qc6yg74faf3d6xty2ofzy:
resolution: { integrity: sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw== }
/@typescript-eslint/eslint-plugin/5.54.0_6mj2wypvdnknez7kws2nfdgupi:
resolution: { integrity: sha512-+hSN9BdSr629RF02d7mMtXhAJvDTyCbprNYJKrXETlul/Aml6YZwd90XioVbjejQeHbb3R8Dg0CkRgoJDxo8aw== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
'@typescript-eslint/parser': ^5.0.0
@ -1233,12 +1234,12 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/parser': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/scope-manager': 5.53.0
'@typescript-eslint/type-utils': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/utils': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
'@typescript-eslint/scope-manager': 5.54.0
'@typescript-eslint/type-utils': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
'@typescript-eslint/utils': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
debug: 4.3.4
eslint: 8.34.0
eslint: 8.35.0
grapheme-splitter: 1.0.4
ignore: 5.2.4
natural-compare-lite: 1.4.0
@ -1250,8 +1251,8 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser/5.53.0_7kw3g6rralp5ps6mg3uyzz6azm:
resolution: { integrity: sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ== }
/@typescript-eslint/parser/5.54.0_ycpbpc6yetojsgtrx3mwntkhsu:
resolution: { integrity: sha512-aAVL3Mu2qTi+h/r04WI/5PfNWvO6pdhpeMRWk9R7rEV4mwJNzoWf5CCU5vDKBsPIFQFjEq1xg7XBI2rjiMXQbQ== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@ -1260,26 +1261,26 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 5.53.0
'@typescript-eslint/types': 5.53.0
'@typescript-eslint/typescript-estree': 5.53.0_typescript@4.9.5
'@typescript-eslint/scope-manager': 5.54.0
'@typescript-eslint/types': 5.54.0
'@typescript-eslint/typescript-estree': 5.54.0_typescript@4.9.5
debug: 4.3.4
eslint: 8.34.0
eslint: 8.35.0
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/scope-manager/5.53.0:
resolution: { integrity: sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w== }
/@typescript-eslint/scope-manager/5.54.0:
resolution: { integrity: sha512-VTPYNZ7vaWtYna9M4oD42zENOBrb+ZYyCNdFs949GcN8Miwn37b8b7eMj+EZaq7VK9fx0Jd+JhmkhjFhvnovhg== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dependencies:
'@typescript-eslint/types': 5.53.0
'@typescript-eslint/visitor-keys': 5.53.0
'@typescript-eslint/types': 5.54.0
'@typescript-eslint/visitor-keys': 5.54.0
dev: true
/@typescript-eslint/type-utils/5.53.0_7kw3g6rralp5ps6mg3uyzz6azm:
resolution: { integrity: sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw== }
/@typescript-eslint/type-utils/5.54.0_ycpbpc6yetojsgtrx3mwntkhsu:
resolution: { integrity: sha512-WI+WMJ8+oS+LyflqsD4nlXMsVdzTMYTxl16myXPaCXnSgc7LWwMsjxQFZCK/rVmTZ3FN71Ct78ehO9bRC7erYQ== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
eslint: '*'
@ -1288,23 +1289,23 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 5.53.0_typescript@4.9.5
'@typescript-eslint/utils': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/typescript-estree': 5.54.0_typescript@4.9.5
'@typescript-eslint/utils': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
debug: 4.3.4
eslint: 8.34.0
eslint: 8.35.0
tsutils: 3.21.0_typescript@4.9.5
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/types/5.53.0:
resolution: { integrity: sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== }
/@typescript-eslint/types/5.54.0:
resolution: { integrity: sha512-nExy+fDCBEgqblasfeE3aQ3NuafBUxZxgxXcYfzYRZFHdVvk5q60KhCSkG0noHgHRo/xQ/BOzURLZAafFpTkmQ== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dev: true
/@typescript-eslint/typescript-estree/5.53.0_typescript@4.9.5:
resolution: { integrity: sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w== }
/@typescript-eslint/typescript-estree/5.54.0_typescript@4.9.5:
resolution: { integrity: sha512-X2rJG97Wj/VRo5YxJ8Qx26Zqf0RRKsVHd4sav8NElhbZzhpBI8jU54i6hfo9eheumj4oO4dcRN1B/zIVEqR/MQ== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
typescript: '*'
@ -1312,8 +1313,8 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 5.53.0
'@typescript-eslint/visitor-keys': 5.53.0
'@typescript-eslint/types': 5.54.0
'@typescript-eslint/visitor-keys': 5.54.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
@ -1324,31 +1325,31 @@ packages:
- supports-color
dev: true
/@typescript-eslint/utils/5.53.0_7kw3g6rralp5ps6mg3uyzz6azm:
resolution: { integrity: sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g== }
/@typescript-eslint/utils/5.54.0_ycpbpc6yetojsgtrx3mwntkhsu:
resolution: { integrity: sha512-cuwm8D/Z/7AuyAeJ+T0r4WZmlnlxQ8wt7C7fLpFlKMR+dY6QO79Cq1WpJhvZbMA4ZeZGHiRWnht7ZJ8qkdAunw== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@types/json-schema': 7.0.11
'@types/semver': 7.3.13
'@typescript-eslint/scope-manager': 5.53.0
'@typescript-eslint/types': 5.53.0
'@typescript-eslint/typescript-estree': 5.53.0_typescript@4.9.5
eslint: 8.34.0
'@typescript-eslint/scope-manager': 5.54.0
'@typescript-eslint/types': 5.54.0
'@typescript-eslint/typescript-estree': 5.54.0_typescript@4.9.5
eslint: 8.35.0
eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@8.34.0
eslint-utils: 3.0.0_eslint@8.35.0
semver: 7.3.8
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/@typescript-eslint/visitor-keys/5.53.0:
resolution: { integrity: sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w== }
/@typescript-eslint/visitor-keys/5.54.0:
resolution: { integrity: sha512-xu4wT7aRCakGINTLGeyGqDn+78BwFlggwBjnHa1ar/KaGagnmwLYmlrXIrgAaQ3AE1Vd6nLfKASm7LrFHNbKGA== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dependencies:
'@typescript-eslint/types': 5.53.0
'@typescript-eslint/types': 5.54.0
eslint-visitor-keys: 3.3.0
dev: true
@ -1609,7 +1610,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.21.5
caniuse-lite: 1.0.30001457
caniuse-lite: 1.0.30001458
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@ -1714,8 +1715,8 @@ packages:
engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 }
hasBin: true
dependencies:
caniuse-lite: 1.0.30001457
electron-to-chromium: 1.4.310
caniuse-lite: 1.0.30001458
electron-to-chromium: 1.4.313
node-releases: 2.0.10
update-browserslist-db: 1.0.10_browserslist@4.21.5
@ -1729,15 +1730,14 @@ packages:
/callsites/3.1.0:
resolution: { integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== }
engines: { node: '>=6' }
dev: true
/camelcase-css/2.0.1:
resolution: { integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== }
engines: { node: '>= 6' }
dev: true
/caniuse-lite/1.0.30001457:
resolution: { integrity: sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA== }
/caniuse-lite/1.0.30001458:
resolution: { integrity: sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w== }
/ccount/2.0.1:
resolution: { integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== }
@ -2030,8 +2030,8 @@ packages:
resolution: { integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== }
dev: true
/electron-to-chromium/1.4.310:
resolution: { integrity: sha512-/xlATgfwkm5uDDwLw5nt/MNEf7c1oazLURMZLy39vOioGYyYzLWIDT8fZMJak6qTiAJ7udFTy7JG7ziyjNutiA== }
/electron-to-chromium/1.4.313:
resolution: { integrity: sha512-QckB9OVqr2oybjIrbMI99uF+b9+iTja5weFe0ePbqLb5BHqXOJUO1SG6kDj/1WtWPRIBr51N153AEq8m7HuIaA== }
/emoji-regex/8.0.0:
resolution: { integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== }
@ -2144,7 +2144,7 @@ packages:
engines: { node: '>=12' }
dev: false
/eslint-config-next/13.2.1_7kw3g6rralp5ps6mg3uyzz6azm:
/eslint-config-next/13.2.1_ycpbpc6yetojsgtrx3mwntkhsu:
resolution: { integrity: sha512-2GAx7EjSiCzJN6H2L/v1kbYrNiwQxzkyjy6eWSjuhAKt+P6d3nVNHGy9mON8ZcYd72w/M8kyMjm4UB9cvijgrw== }
peerDependencies:
eslint: ^7.23.0 || ^8.0.0
@ -2155,27 +2155,27 @@ packages:
dependencies:
'@next/eslint-plugin-next': 13.2.1
'@rushstack/eslint-patch': 1.2.0
'@typescript-eslint/parser': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
eslint: 8.34.0
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
eslint: 8.35.0
eslint-import-resolver-node: 0.3.7
eslint-import-resolver-typescript: 3.5.3_mvgyw3chnqkp6sgfmmtihyjpnm
eslint-plugin-import: 2.27.5_2hqppaeqs2axgzqg6vttejknky
eslint-plugin-jsx-a11y: 6.7.1_eslint@8.34.0
eslint-plugin-react: 7.32.2_eslint@8.34.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.34.0
eslint-import-resolver-typescript: 3.5.3_yckic57kx266ph64dhq6ozvb54
eslint-plugin-import: 2.27.5_tqrcrxlenpngfto46ddarus52y
eslint-plugin-jsx-a11y: 6.7.1_eslint@8.35.0
eslint-plugin-react: 7.32.2_eslint@8.35.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.35.0
typescript: 4.9.5
transitivePeerDependencies:
- eslint-import-resolver-webpack
- supports-color
dev: true
/eslint-config-prettier/8.6.0_eslint@8.34.0:
/eslint-config-prettier/8.6.0_eslint@8.35.0:
resolution: { integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA== }
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
dependencies:
eslint: 8.34.0
eslint: 8.35.0
dev: true
/eslint-import-resolver-node/0.3.7:
@ -2188,7 +2188,7 @@ packages:
- supports-color
dev: true
/eslint-import-resolver-typescript/3.5.3_mvgyw3chnqkp6sgfmmtihyjpnm:
/eslint-import-resolver-typescript/3.5.3_yckic57kx266ph64dhq6ozvb54:
resolution: { integrity: sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ== }
engines: { node: ^14.18.0 || >=16.0.0 }
peerDependencies:
@ -2197,8 +2197,8 @@ packages:
dependencies:
debug: 4.3.4
enhanced-resolve: 5.12.0
eslint: 8.34.0
eslint-plugin-import: 2.27.5_2hqppaeqs2axgzqg6vttejknky
eslint: 8.35.0
eslint-plugin-import: 2.27.5_tqrcrxlenpngfto46ddarus52y
get-tsconfig: 4.4.0
globby: 13.1.3
is-core-module: 2.11.0
@ -2208,7 +2208,7 @@ packages:
- supports-color
dev: true
/eslint-module-utils/2.7.4_yfzt44nswbaazp63chcrlz6vvq:
/eslint-module-utils/2.7.4_igrub7c6rucg6hjc3uqgumd66y:
resolution: { integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== }
engines: { node: '>=4' }
peerDependencies:
@ -2229,16 +2229,16 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
debug: 3.2.7
eslint: 8.34.0
eslint: 8.35.0
eslint-import-resolver-node: 0.3.7
eslint-import-resolver-typescript: 3.5.3_mvgyw3chnqkp6sgfmmtihyjpnm
eslint-import-resolver-typescript: 3.5.3_yckic57kx266ph64dhq6ozvb54
transitivePeerDependencies:
- supports-color
dev: true
/eslint-plugin-import/2.27.5_2hqppaeqs2axgzqg6vttejknky:
/eslint-plugin-import/2.27.5_tqrcrxlenpngfto46ddarus52y:
resolution: { integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== }
engines: { node: '>=4' }
peerDependencies:
@ -2248,15 +2248,15 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
array-includes: 3.1.6
array.prototype.flat: 1.3.1
array.prototype.flatmap: 1.3.1
debug: 3.2.7
doctrine: 2.1.0
eslint: 8.34.0
eslint: 8.35.0
eslint-import-resolver-node: 0.3.7
eslint-module-utils: 2.7.4_yfzt44nswbaazp63chcrlz6vvq
eslint-module-utils: 2.7.4_igrub7c6rucg6hjc3uqgumd66y
has: 1.0.3
is-core-module: 2.11.0
is-glob: 4.0.3
@ -2264,14 +2264,14 @@ packages:
object.values: 1.1.6
resolve: 1.22.1
semver: 6.3.0
tsconfig-paths: 3.14.1
tsconfig-paths: 3.14.2
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
dev: true
/eslint-plugin-jsx-a11y/6.7.1_eslint@8.34.0:
/eslint-plugin-jsx-a11y/6.7.1_eslint@8.35.0:
resolution: { integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== }
engines: { node: '>=4.0' }
peerDependencies:
@ -2286,7 +2286,7 @@ packages:
axobject-query: 3.1.1
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
eslint: 8.34.0
eslint: 8.35.0
has: 1.0.3
jsx-ast-utils: 3.3.3
language-tags: 1.0.5
@ -2296,16 +2296,16 @@ packages:
semver: 6.3.0
dev: true
/eslint-plugin-react-hooks/4.6.0_eslint@8.34.0:
/eslint-plugin-react-hooks/4.6.0_eslint@8.35.0:
resolution: { integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== }
engines: { node: '>=10' }
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
dependencies:
eslint: 8.34.0
eslint: 8.35.0
dev: true
/eslint-plugin-react/7.32.2_eslint@8.34.0:
/eslint-plugin-react/7.32.2_eslint@8.35.0:
resolution: { integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg== }
engines: { node: '>=4' }
peerDependencies:
@ -2315,7 +2315,7 @@ packages:
array.prototype.flatmap: 1.3.1
array.prototype.tosorted: 1.1.1
doctrine: 2.1.0
eslint: 8.34.0
eslint: 8.35.0
estraverse: 5.3.0
jsx-ast-utils: 3.3.3
minimatch: 3.1.2
@ -2345,13 +2345,13 @@ packages:
estraverse: 5.3.0
dev: true
/eslint-utils/3.0.0_eslint@8.34.0:
/eslint-utils/3.0.0_eslint@8.35.0:
resolution: { integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== }
engines: { node: ^10.0.0 || ^12.0.0 || >= 14.0.0 }
peerDependencies:
eslint: '>=5'
dependencies:
eslint: 8.34.0
eslint: 8.35.0
eslint-visitor-keys: 2.1.0
dev: true
@ -2365,12 +2365,13 @@ packages:
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dev: true
/eslint/8.34.0:
resolution: { integrity: sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== }
/eslint/8.35.0:
resolution: { integrity: sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw== }
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
hasBin: true
dependencies:
'@eslint/eslintrc': 1.4.1
'@eslint/eslintrc': 2.0.0
'@eslint/js': 8.35.0
'@humanwhocodes/config-array': 0.11.8
'@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8
@ -2381,7 +2382,7 @@ packages:
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.1.1
eslint-utils: 3.0.0_eslint@8.34.0
eslint-utils: 3.0.0_eslint@8.35.0
eslint-visitor-keys: 3.3.0
espree: 9.4.1
esquery: 1.4.2
@ -2734,7 +2735,6 @@ packages:
/graceful-fs/4.2.10:
resolution: { integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== }
dev: true
/grapheme-splitter/1.0.4:
resolution: { integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== }
@ -3187,53 +3187,18 @@ packages:
resolution: { integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== }
dev: true
/isomorphic-ws/5.0.0_ws@8.12.1:
resolution: { integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== }
peerDependencies:
ws: '*'
dependencies:
ws: 8.12.1
dev: false
/javascript-natural-sort/0.7.1:
resolution: { integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw== }
dev: true
/jotai/1.13.1_react@18.2.0:
resolution: { integrity: sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw== }
engines: { node: '>=12.20.0' }
peerDependencies:
'@babel/core': '*'
'@babel/template': '*'
jotai-devtools: '*'
jotai-immer: '*'
jotai-optics: '*'
jotai-redux: '*'
jotai-tanstack-query: '*'
jotai-urql: '*'
jotai-valtio: '*'
jotai-xstate: '*'
jotai-zustand: '*'
react: '>=16.8'
peerDependenciesMeta:
'@babel/core':
optional: true
'@babel/template':
optional: true
jotai-devtools:
optional: true
jotai-immer:
optional: true
jotai-optics:
optional: true
jotai-redux:
optional: true
jotai-tanstack-query:
optional: true
jotai-urql:
optional: true
jotai-valtio:
optional: true
jotai-xstate:
optional: true
jotai-zustand:
optional: true
dependencies:
react: 18.2.0
dev: false
/js-sdsl/4.3.0:
resolution: { integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== }
dev: true
@ -3447,7 +3412,7 @@ packages:
dependencies:
'@types/mdast': 3.0.10
escape-string-regexp: 5.0.0
unist-util-is: 5.2.0
unist-util-is: 5.2.1
unist-util-visit-parents: 5.1.3
dev: false
@ -3530,7 +3495,7 @@ packages:
resolution: { integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== }
dependencies:
'@types/mdast': 3.0.10
unist-util-is: 5.2.0
unist-util-is: 5.2.1
dev: false
/mdast-util-to-hast/12.3.0:
@ -3923,7 +3888,7 @@ packages:
dependencies:
'@next/env': 13.2.1
'@swc/helpers': 0.4.14
caniuse-lite: 1.0.30001457
caniuse-lite: 1.0.30001458
postcss: 8.4.14
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
@ -3960,31 +3925,19 @@ packages:
engines: { node: '>=0.10.0' }
dev: true
/nostr-react/0.6.4_react@18.2.0:
resolution: { integrity: sha512-esRgmhTP5kPQ8ufs8cFAQxxJtMmzuba/k2QfXevG/ejHP3IMa41pb82qi8V0aPzY3KJ0Nr54x0OSa39d2InKzA== }
engines: { node: '>=12' }
peerDependencies:
react: '>=16'
/nostr-relaypool/0.5.3_ws@8.12.1:
resolution: { integrity: sha512-1INGKleOTuUTFUs3RnnZrew4+G/idLUewh44WBtmTTJ9g+kRiQtMMaBGTVUpf9621nBNleEVOB8p3XSNcaX3FQ== }
dependencies:
jotai: 1.13.1_react@18.2.0
nostr-tools: 1.6.0
react: 18.2.0
'@jest/source-map': 29.4.3
isomorphic-ws: 5.0.0_ws@8.12.1
nostr-tools: 1.7.1
safe-stable-stringify: 2.4.2
transitivePeerDependencies:
- '@babel/core'
- '@babel/template'
- jotai-devtools
- jotai-immer
- jotai-optics
- jotai-redux
- jotai-tanstack-query
- jotai-urql
- jotai-valtio
- jotai-xstate
- jotai-zustand
- ws
dev: false
/nostr-tools/1.6.0:
resolution: { integrity: sha512-qjjJQ7YxJUMzgS24eVlxkZ87PKJtU6dlH04OzVuK6w+GSPL+VdUZkMe2lfSpnb7OkCrDIzmbFbtx+Q4LXdU2xw== }
/nostr-tools/1.7.1:
resolution: { integrity: sha512-r72KpbLVz6Gaqei6LIj6m+cyp24eF3myiIMlmv93WCgDFCI5w72u+OrZzjSrJaeE94vYoEJfOF16/2Rl5o5z5w== }
dependencies:
'@noble/hashes': 1.0.0
'@noble/secp256k1': 1.7.1
@ -4336,7 +4289,7 @@ packages:
engines: { node: '>= 0.8.0' }
dev: true
/prettier-plugin-tailwindcss/0.2.3_3p4xqifn6m4d44r76wgcnqfi3i:
/prettier-plugin-tailwindcss/0.2.3_zmkqdpv3ldc45e6wei6qtrbrca:
resolution: { integrity: sha512-s2N5Dh7Ao5KTV1mao5ZBnn8EKtUcDPJEkGViZIjI0Ij9TTI5zgTz4IHOxW33jOdjHKa8CSjM88scelUiC5TNRQ== }
engines: { node: '>=12.17.0' }
peerDependencies:
@ -4388,7 +4341,7 @@ packages:
prettier-plugin-twig-melody:
optional: true
dependencies:
'@trivago/prettier-plugin-sort-imports': 4.1.0_prettier@2.8.4
'@trivago/prettier-plugin-sort-imports': 4.1.1_prettier@2.8.4
prettier: 2.8.4
dev: true
@ -4824,6 +4777,11 @@ packages:
is-regex: 1.1.4
dev: true
/safe-stable-stringify/2.4.2:
resolution: { integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA== }
engines: { node: '>=10' }
dev: false
/scheduler/0.23.0:
resolution: { integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== }
dependencies:
@ -5146,8 +5104,8 @@ packages:
resolution: { integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== }
dev: false
/tsconfig-paths/3.14.1:
resolution: { integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== }
/tsconfig-paths/3.14.2:
resolution: { integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== }
dependencies:
'@types/json5': 0.0.29
json5: 1.0.2
@ -5233,7 +5191,7 @@ packages:
resolution: { integrity: sha512-RynicUM/vbOSTSiUK+BnaK9XMfmQUh6gyi7L6taNgc7FIf84GukXVV3ucGzEN/PhUUkdP5hb1MmXc+3cvPUm5Q== }
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.2.0
unist-util-is: 5.2.1
unist-util-visit-parents: 5.1.3
dev: false
@ -5241,8 +5199,10 @@ packages:
resolution: { integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== }
dev: false
/unist-util-is/5.2.0:
resolution: { integrity: sha512-Glt17jWwZeyqrFqOK0pF1Ded5U3yzJnFr8CG1GMjCWTp9zDo2p+cmD6pWbZU8AgM5WU3IzRv6+rBwhzsGh6hBQ== }
/unist-util-is/5.2.1:
resolution: { integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== }
dependencies:
'@types/unist': 2.0.6
dev: false
/unist-util-position/4.0.4:
@ -5261,14 +5221,14 @@ packages:
resolution: { integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== }
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.2.0
unist-util-is: 5.2.1
dev: false
/unist-util-visit/4.1.2:
resolution: { integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== }
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.2.0
unist-util-is: 5.2.1
unist-util-visit-parents: 5.1.3
dev: false
@ -5331,14 +5291,6 @@ packages:
tslib: 2.5.0
dev: false
/use-sync-external-store/1.2.0_react@18.2.0:
resolution: { integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== }
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/util-deprecate/1.0.2:
resolution: { integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== }
dev: true
@ -5447,6 +5399,19 @@ packages:
resolution: { integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== }
dev: true
/ws/8.12.1:
resolution: { integrity: sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== }
engines: { node: '>=10.0.0' }
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false
/xtend/4.0.2:
resolution: { integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== }
engines: { node: '>=0.4' }

168
src-tauri/Cargo.lock generated
View File

@ -465,36 +465,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
[[package]]
name = "curl"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2",
"winapi",
]
[[package]]
name = "curl-sys"
version = "0.4.59+curl-7.86.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"winapi",
]
[[package]]
name = "darling"
version = "0.13.4"
@ -1393,6 +1363,30 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libappindicator"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8"
dependencies = [
"glib",
"gtk",
"gtk-sys",
"libappindicator-sys",
"log",
]
[[package]]
name = "libappindicator-sys"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa"
dependencies = [
"gtk-sys",
"libloading",
"once_cell",
]
[[package]]
name = "libc"
version = "0.2.139"
@ -1408,6 +1402,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libloading"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
]
[[package]]
name = "libsqlite3-sys"
version = "0.24.2"
@ -1419,18 +1423,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "line-wrap"
version = "0.1.1"
@ -1485,7 +1477,6 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-sql",
"webpage",
]
[[package]]
@ -1504,7 +1495,7 @@ dependencies = [
"dirs-next",
"objc-foundation",
"objc_id",
"time 0.3.17",
"time",
]
[[package]]
@ -1530,18 +1521,6 @@ dependencies = [
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
dependencies = [
"html5ever",
"markup5ever",
"tendril",
"xml5ever",
]
[[package]]
name = "matchers"
version = "0.1.0"
@ -1795,25 +1774,6 @@ dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -2068,7 +2028,7 @@ dependencies = [
"line-wrap",
"quick-xml 0.26.0",
"serde",
"time 0.3.17",
"time",
]
[[package]]
@ -2421,15 +2381,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@ -2917,6 +2868,7 @@ dependencies = [
"core-foundation",
"core-graphics",
"crossbeam-channel",
"dirs-next",
"dispatch",
"gdk",
"gdk-pixbuf",
@ -2930,6 +2882,7 @@ dependencies = [
"instant",
"jni",
"lazy_static",
"libappindicator",
"libc",
"log",
"ndk",
@ -3047,7 +3000,7 @@ dependencies = [
"sha2",
"tauri-utils",
"thiserror",
"time 0.3.17",
"time",
"uuid 1.3.0",
"walkdir",
]
@ -3069,7 +3022,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-sql"
version = "0.1.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=fix/sql-types#f1a7136b1e0b145ea5c5fb9d336cac0cb1dc6265"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=dev#8f34eb83e4f9a8c72fd3823a066c94f861f2d021"
dependencies = [
"futures",
"log",
@ -3220,17 +3173,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.17"
@ -3540,12 +3482,6 @@ version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -3675,18 +3611,6 @@ dependencies = [
"system-deps 6.0.3",
]
[[package]]
name = "webpage"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d261bbae112cb48a95d3cc9e8873a4e40933bc54ae8eddc1eef70e952dd3b232"
dependencies = [
"curl",
"html5ever",
"markup5ever_rcdom",
"serde_json",
]
[[package]]
name = "webpki"
version = "0.22.0"
@ -4051,15 +3975,3 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [
"libc",
]
[[package]]
name = "xml5ever"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865"
dependencies = [
"log",
"mac",
"markup5ever",
"time 0.1.45",
]

View File

@ -16,12 +16,11 @@ tauri-build = { version = "1.2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["clipboard-all", "notification-all", "shell-open", "window-start-dragging"] }
webpage = "1.5.0"
tauri = { version = "1.2", features = ["clipboard-all", "notification-all", "shell-open", "system-tray", "window-start-dragging"] }
[dependencies.tauri-plugin-sql]
git = "https://github.com/tauri-apps/plugins-workspace"
branch = "fix/sql-types"
branch = "dev"
features = ["sqlite"]
[target.'cfg(target_os = "macos")'.dependencies]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -0,0 +1,70 @@
-- Add migration script here
-- create relays
CREATE TABLE
relays (
id INTEGER PRIMARY KEY,
relay_url TEXT NOT NULL,
relay_status INTEGER NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO
relays (relay_url, relay_status)
VALUES
("wss://relay.damus.io", "1"),
("wss://relay.uselume.xyz", "0"),
("wss://nostr-pub.wellorder.net", "1"),
("wss://nostr.bongbong.com", "1"),
("wss://nostr.zebedee.cloud", "1"),
("wss://nostr.fmt.wiz.biz", "1"),
("wss://nostr.walletofsatoshi.com", "1"),
("wss://relay.snort.social", "1"),
("wss://offchain.pub", "1"),
("wss://nos.lol", "1");
-- create accounts
CREATE TABLE
accounts (
id TEXT PRIMARY KEY,
privkey TEXT NOT NULL,
npub TEXT NOT NULL,
nsec TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 0,
metadata JSON
);
-- create follows
CREATE TABLE
follows (
id INTEGER PRIMARY KEY,
pubkey TEXT NOT NULL,
account TEXT NOT NULL,
kind INTEGER NOT NULL DEFAULT 0,
metadata JSON
);
-- create index for pubkey in follows
CREATE UNIQUE INDEX index_pubkey ON follows (pubkey);
-- create cache profiles
CREATE TABLE
cache_profiles (
id TEXT PRIMARY KEY,
metadata JSON,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- create cache notes
CREATE TABLE
cache_notes (
id TEXT PRIMARY KEY,
pubkey TEXT NOT NULL,
created_at TEXT,
kind INTEGER NOT NULL DEFAULT 1,
tags TEXT NOT NULL,
content TEXT NOT NULL,
relay TEXT,
is_multi BOOLEAN DEFAULT 0
);

View File

@ -7,48 +7,15 @@
#[macro_use]
extern crate objc;
use std::time::Duration;
use tauri::{Manager, WindowEvent};
use webpage::{Webpage, WebpageOptions};
use tauri::{Manager, SystemTray, WindowEvent};
use tauri_plugin_sql::{Migration, MigrationKind};
use window_ext::WindowExt;
mod window_ext;
#[derive(serde::Serialize)]
struct OpenGraphResponse {
title: String,
description: String,
url: String,
image: String,
}
async fn fetch_opengraph(url: String) -> OpenGraphResponse {
let options = WebpageOptions {
allow_insecure: false,
max_redirections: 3,
timeout: Duration::from_secs(30),
useragent: "lume - desktop app".to_string(),
..Default::default()
};
let result = Webpage::from_url(&url, options).expect("Could not read from URL");
let html = result.html;
return OpenGraphResponse {
title: html.opengraph.properties["title"].to_string(),
description: html.opengraph.properties["description"].to_string(),
url: html.opengraph.properties["url"].to_string(),
image: html.opengraph.images[0].url.to_string(),
};
}
#[tauri::command]
async fn opengraph(url: String) -> OpenGraphResponse {
let result = fetch_opengraph(url).await;
return result;
}
fn main() {
let tray = SystemTray::new();
tauri::Builder::default()
.setup(|app| {
let main_window = app.get_window("main").unwrap();
@ -57,8 +24,20 @@ fn main() {
Ok(())
})
.invoke_handler(tauri::generate_handler![opengraph])
.plugin(tauri_plugin_sql::Builder::default().build())
.system_tray(tray)
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations(
"sqlite:lume.db",
vec![Migration {
version: 1,
description: "create default tables",
sql: include_str!("../migrations/20230226004139_create_tables.sql"),
kind: MigrationKind::Up,
}],
)
.build(),
)
.on_window_event(|e| {
let apply_offset = || {
let win = e.window();

View File

@ -37,13 +37,7 @@
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"identifier": "com.uselume.xyz",
"longDescription": "",
"macOS": {
@ -68,6 +62,10 @@
"updater": {
"active": false
},
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"windows": [
{
"theme": "Dark",

View File

@ -16,12 +16,7 @@ export const Account = memo(function Account({ user, current }: { user: any; cur
current === user.pubkey ? 'ring-1 ring-fuchsia-500 ring-offset-4 ring-offset-black' : ''
}`}>
{userData?.picture !== undefined ? (
<Image
src={userData.picture}
alt="user's avatar"
fill={true}
className="rounded-full object-cover"
/>
<Image src={userData.picture} alt="user's avatar" fill={true} className="rounded-full object-cover" />
) : (
<div className="h-11 w-11 animate-pulse rounded-full bg-zinc-700" />
)}

View File

@ -1,19 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Account } from '@components/accountBar/account';
import { currentUser } from '@stores/currentUser';
import LumeSymbol from '@assets/icons/Lume';
import { useStore } from '@nanostores/react';
import { PlusIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
export default function AccountBar() {
const [users, setUsers] = useState([]);
const $currentUser: any = useStore(currentUser);
const [currentUser]: any = useLocalStorage('current-user');
const getAccounts = useCallback(async () => {
const db = await Database.load('sqlite:lume.db');
@ -30,7 +27,7 @@ export default function AccountBar() {
<div className="flex h-full flex-col items-center justify-between px-2 pt-12 pb-4">
<div className="flex flex-col gap-4">
{users.map((user, index) => (
<Account key={index} user={user} current={$currentUser.pubkey} />
<Account key={index} user={user} current={currentUser.pubkey} />
))}
<Link
href="/onboarding"

View File

@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { writeStorage } from '@rehooks/local-storage';
import { createContext, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
export const DatabaseContext = createContext({});
const db = typeof window !== 'undefined' ? await Database.load('sqlite:lume.db') : null;
export default function DatabaseProvider({ children }: { children: React.ReactNode }) {
const [done, setDone] = useState(false);
useEffect(() => {
const getRelays = async () => {
const arr = [];
const result: any[] = await db.select('SELECT relay_url FROM relays WHERE relay_status = "1"');
result.forEach((item: { relay_url: string }) => {
arr.push(item.relay_url);
});
writeStorage('relays', arr);
};
const getAccount = async () => {
const result = await db.select(`SELECT * FROM accounts LIMIT 1`);
writeStorage('current-user', result[0]);
return result[0];
};
const getFollows = async (id: string) => {
const arr = [];
const result: any[] = await db.select(`SELECT pubkey FROM follows WHERE account = "${id}"`);
result.forEach((item: { pubkey: string }) => {
arr.push(item.pubkey);
});
writeStorage('follows', arr);
};
getRelays().catch(console.error);
getAccount()
.then((res) => {
if (res) {
getFollows(res.id).catch(console.error);
}
setDone(true);
})
.catch(console.error);
}, []);
if (done === true) {
return <DatabaseContext.Provider value={{ db }}>{children}</DatabaseContext.Provider>;
}
}

View File

@ -0,0 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { RelayPool } from 'nostr-relaypool';
import { createContext, useMemo } from 'react';
export const RelayContext = createContext({});
export default function RelayProvider({ relays, children }: { relays: any; children: React.ReactNode }) {
const value = useMemo(() => new RelayPool(relays, { useEventCache: true }), [relays]);
return <RelayContext.Provider value={value}>{children}</RelayContext.Provider>;
}

View File

@ -1,25 +1,28 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { currentUser } from '@stores/currentUser';
import { RelayContext } from '@components/contexts/relay';
import { dateToUnix } from '@utils/getDate';
import { useStore } from '@nanostores/react';
import * as Dialog from '@radix-ui/react-dialog';
import { useLocalStorage } from '@rehooks/local-storage';
import * as commands from '@uiw/react-md-editor/lib/commands';
import dynamic from 'next/dynamic';
import { dateToUnix, useNostr } from 'nostr-react';
import { getEventHash, signEvent } from 'nostr-tools';
import { useState } from 'react';
import { useContext, useState } from 'react';
const MDEditor = dynamic(() => import('@uiw/react-md-editor').then((mod) => mod.default), {
ssr: false,
});
export default function CreatePost() {
const { publish } = useNostr();
const relayPool: any = useContext(RelayContext);
const [relays]: any = useLocalStorage('relays');
const [value, setValue] = useState('');
const $currentUser: any = useStore(currentUser);
const pubkey = $currentUser.pubkey;
const privkey = $currentUser.privkey;
const [currentUser]: any = useLocalStorage('current-user');
const pubkey = currentUser.pubkey;
const privkey = currentUser.privkey;
const postButton = {
name: 'post',
@ -27,9 +30,7 @@ export default function CreatePost() {
buttonProps: { className: 'cta-btn', 'aria-label': 'Post a message' },
icon: (
<div className="relative inline-flex h-10 w-16 transform cursor-pointer overflow-hidden rounded bg-zinc-900 px-2.5 ring-zinc-500/50 ring-offset-zinc-900 will-change-transform focus:outline-none focus:ring-1 focus:ring-offset-2 active:translate-y-1">
<span className="absolute inset-px z-10 inline-flex items-center justify-center rounded bg-zinc-900 text-zinc-200">
Post
</span>
<span className="absolute inset-px z-10 inline-flex items-center justify-center rounded bg-zinc-900 text-zinc-200">Post</span>
<span className="absolute inset-0 z-0 scale-x-[2.0] blur before:absolute before:inset-0 before:top-1/2 before:aspect-square before:animate-disco before:bg-gradient-conic before:from-gray-300 before:via-fuchsia-600 before:to-orange-600"></span>
</div>
),
@ -44,11 +45,10 @@ export default function CreatePost() {
pubkey: pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, privkey);
publish(event);
relayPool.publish(event, relays);
setValue('');
}
},

View File

@ -3,30 +3,22 @@ import ActiveLink from '@components/activeLink';
import CreatePost from '@components/navigatorBar/createPost';
import { ProfileMenu } from '@components/navigatorBar/profileMenu';
import { currentUser } from '@stores/currentUser';
import { useStore } from '@nanostores/react';
import { PlusIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
export default function NavigatorBar() {
const $currentUser: any = useStore(currentUser);
const profile =
$currentUser.metadata !== undefined
? JSON.parse($currentUser.metadata)
: { display_name: null, username: null };
const [currentUser]: any = useLocalStorage('current-user');
const profile = JSON.parse(currentUser.metadata);
return (
<div className="flex h-full flex-col flex-wrap justify-between overflow-hidden px-2 pt-3 pb-4">
{/* main */}
<div className="flex flex-col gap-4">
{/* Create post */}
<div className="flex flex-col rounded-lg bg-zinc-900 ring-1 ring-white/10">
<div className="flex flex-col p-2">
<div className="flex items-center justify-between">
<h5 className="font-semibold leading-tight text-zinc-100">
{profile.display_name || ''}
</h5>
<ProfileMenu pubkey={$currentUser.pubkey} />
<h5 className="font-semibold leading-tight text-zinc-100">{profile.display_name || ''}</h5>
<ProfileMenu pubkey={currentUser.pubkey} />
</div>
<span className="text-sm leading-tight text-zinc-500">@{profile.username || ''}</span>
</div>
@ -38,9 +30,7 @@ export default function NavigatorBar() {
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between px-2">
<h3 className="text-sm font-bold text-zinc-400">Newsfeed</h3>
<button
type="button"
className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900">
<button type="button" className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900">
<PlusIcon className="h-3 w-3 text-zinc-400 group-hover:text-zinc-100" />
</button>
</div>
@ -65,9 +55,7 @@ export default function NavigatorBar() {
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between px-2">
<h3 className="text-sm font-bold text-zinc-400">Direct Messages</h3>
<button
type="button"
className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900">
<button type="button" className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900">
<PlusIcon className="h-3 w-3 text-zinc-400 group-hover:text-zinc-100" />
</button>
</div>
@ -77,17 +65,3 @@ export default function NavigatorBar() {
</div>
);
}
/* Channels
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between px-2">
<h3 className="text-sm font-bold text-zinc-400">Channels</h3>
<button
type="button"
className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900">
<PlusIcon className="h-4 w-4 text-zinc-400 group-hover:text-zinc-100" />
</button>
</div>
<div></div>
</div>
*/

View File

@ -1,46 +1,46 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { currentUser } from '@stores/currentUser';
import { RelayContext } from '@components/contexts/relay';
import { dateToUnix } from '@utils/getDate';
import { useStore } from '@nanostores/react';
import { HeartFilledIcon, HeartIcon } from '@radix-ui/react-icons';
import { dateToUnix, useNostr, useNostrEvents } from 'nostr-react';
import { useLocalStorage } from '@rehooks/local-storage';
import { getEventHash, signEvent } from 'nostr-tools';
import { useState } from 'react';
import { useContext, useState } from 'react';
export default function Reaction({ eventID, eventPubkey }: { eventID: string; eventPubkey: string }) {
const relayPool: any = useContext(RelayContext);
const [relays]: any = useLocalStorage('relays');
export default function Reaction({
eventID,
eventPubkey,
}: {
eventID: string;
eventPubkey: string;
}) {
const { publish } = useNostr();
const [reaction, setReaction] = useState(0);
const [isReact, setIsReact] = useState(false);
const $currentUser: any = useStore(currentUser);
const pubkey = $currentUser.pubkey;
const privkey = $currentUser.privkey;
const [currentUser]: any = useLocalStorage('current-user');
const pubkey = currentUser.pubkey;
const privkey = currentUser.privkey;
const { onEvent } = useNostrEvents({
filter: {
'#e': [eventID],
since: 0,
kinds: [7],
limit: 20,
},
});
onEvent((rawMetadata) => {
try {
const content = rawMetadata.content;
if (content === '🤙' || content === '+') {
/*
relayPool.subscribe(
[
{
'#e': [eventID],
since: 0,
kinds: [7],
limit: 10,
},
],
relays,
(event: any) => {
if (event.content === '🤙' || event.content === '+') {
setReaction(reaction + 1);
}
} catch (err) {
console.error(err, rawMetadata);
},
undefined,
(events: any, relayURL: any) => {
console.log(events, relayURL);
}
});
);
*/
const handleReaction = (e: any) => {
e.stopPropagation();
@ -58,22 +58,16 @@ export default function Reaction({
event.id = getEventHash(event);
event.sig = signEvent(event, privkey);
publish(event);
relayPool.publish(event, relays);
setIsReact(true);
setReaction(reaction + 1);
};
return (
<button
onClick={(e) => handleReaction(e)}
className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
<button onClick={(e) => handleReaction(e)} className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
<div className="rounded-lg p-1 group-hover:bg-zinc-600">
{isReact ? (
<HeartFilledIcon className="h-4 w-4 group-hover:text-red-400" />
) : (
<HeartIcon className="h-4 w-4 text-zinc-500" />
)}
{isReact ? <HeartFilledIcon className="h-4 w-4 group-hover:text-red-400" /> : <HeartIcon className="h-4 w-4 text-zinc-500" />}
</div>
<span>{reaction}</span>
</button>

View File

@ -1,22 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ChatBubbleIcon } from '@radix-ui/react-icons';
import { useNostrEvents } from 'nostr-react';
import { useState } from 'react';
export default function Reply({ eventID }: { eventID: string }) {
const { events } = useNostrEvents({
filter: {
'#e': [eventID],
since: 0,
kinds: [1],
limit: 10,
},
});
export default function Reply() {
const [count] = useState(0);
return (
<button className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
<div className="rounded-lg p-1 group-hover:bg-zinc-600">
<ChatBubbleIcon className="h-4 w-4 group-hover:text-orange-400" />
</div>
<span>{events.length || 0}</span>
<span>{count}</span>
</button>
);
}

View File

@ -1,88 +1,65 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DatabaseContext } from '@components/contexts/database';
import { ImageWithFallback } from '@components/imageWithFallback';
import { truncate } from '@utils/truncate';
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
import Avatar from 'boring-avatars';
import { useNostrEvents } from 'nostr-react';
import { memo, useEffect, useState } from 'react';
import { memo, useCallback, useContext, useEffect, useState } from 'react';
import Moment from 'react-moment';
import Database from 'tauri-plugin-sql-api';
const db = typeof window !== 'undefined' ? await Database.load('sqlite:lume.db') : null;
export const User = memo(function User({ pubkey, time }: { pubkey: string; time: any }) {
const { db }: any = useContext(DatabaseContext);
const [profile, setProfile] = useState({ picture: null, name: null, username: null });
const { onEvent } = useNostrEvents({
filter: {
authors: [pubkey],
kinds: [0],
},
});
const insertCacheProfile = useCallback(
async (event) => {
const metadata: any = JSON.parse(event.content);
onEvent(async (rawMetadata) => {
try {
const metadata: any = JSON.parse(rawMetadata.content);
if (profile.picture === null || profile.name === null) {
setProfile(metadata);
await db.execute(
`INSERT OR IGNORE INTO cache_profiles (pubkey, metadata) VALUES ("${pubkey}", '${JSON.stringify(
metadata
)}')`
);
} else {
return;
}
} catch (err) {
console.error(err, rawMetadata);
}
});
await db.execute(`INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES ("${pubkey}", '${JSON.stringify(metadata)}')`);
setProfile(metadata);
},
[db, pubkey]
);
const getCacheProfile = useCallback(async () => {
const result: any = await db.select(`SELECT metadata FROM cache_profiles WHERE id = "${pubkey}"`);
return result;
}, [db, pubkey]);
useEffect(() => {
const initialProfile = async () => {
const result: any = await db.select(
`SELECT metadata FROM cache_profiles WHERE pubkey = "${pubkey}"`
);
db.close;
return result;
};
initialProfile()
getCacheProfile()
.then((res) => {
if (res[0] !== undefined) {
setProfile(JSON.parse(res[0].metadata));
} else {
fetch(`https://rbr.bio/${pubkey}/metadata.json`).then((res) =>
res.json().then((res) => {
// update state
setProfile(JSON.parse(res.content));
// save profile to database
insertCacheProfile(res);
})
);
}
})
.catch(console.error);
}, [pubkey]);
}, [getCacheProfile, insertCacheProfile, pubkey]);
return (
<div className="relative flex items-start gap-4">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
{profile.picture ? (
<ImageWithFallback
src={profile.picture}
alt={pubkey}
fill={true}
className="rounded-full object-cover"
/>
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
) : (
<Avatar
size={44}
name={pubkey}
variant="beam"
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
/>
<Avatar size={44} name={pubkey} variant="beam" colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']} />
)}
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full justify-between">
<div className="flex items-baseline gap-2 text-sm">
<span className="font-bold leading-tight">
{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}
</span>
<span className="font-bold leading-tight">{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}</span>
<span className="leading-tight text-zinc-500">·</span>
<Moment fromNow unix className="text-zinc-500">
{time}

View File

@ -1,30 +1,19 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { truncate } from '@utils/truncate';
import { useNostrEvents } from 'nostr-react';
import { memo, useState } from 'react';
import { memo, useEffect, useState } from 'react';
export const UserRepost = memo(function UserRepost({ pubkey }: { pubkey: string }) {
const [profile, setProfile] = useState({ picture: null, name: null });
const { onEvent } = useNostrEvents({
filter: {
authors: [pubkey],
kinds: [0],
},
});
// #TODO: save response to DB
onEvent((rawMetadata) => {
try {
const metadata: any = JSON.parse(rawMetadata.content);
if (metadata) {
setProfile(metadata);
}
} catch (err) {
console.error(err, rawMetadata);
}
});
useEffect(() => {
fetch(`https://rbr.bio/${pubkey}/metadata.json`).then((res) =>
res.json().then((res) => {
// update state
setProfile(JSON.parse(res.content));
})
);
}, [pubkey]);
return (
<div className="text-zinc-400">

View File

@ -5,86 +5,34 @@ import { truncate } from '@utils/truncate';
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
import Avatar from 'boring-avatars';
import { useNostrEvents } from 'nostr-react';
import { memo, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
const db = typeof window !== 'undefined' ? await Database.load('sqlite:lume.db') : null;
export const UserWithUsername = memo(function UserWithUsername({ pubkey }: { pubkey: string }) {
const [profile, setProfile] = useState({ picture: null, name: null, username: null });
const { onEvent } = useNostrEvents({
filter: {
authors: [pubkey],
kinds: [0],
},
});
onEvent(async (rawMetadata) => {
try {
const metadata: any = JSON.parse(rawMetadata.content);
if (profile.picture === null || profile.name === null) {
setProfile(metadata);
await db.execute(
`INSERT OR IGNORE INTO cache_profiles (pubkey, metadata) VALUES ("${pubkey}", '${JSON.stringify(
metadata
)}')`
);
} else {
return;
}
} catch (err) {
console.error(err, rawMetadata);
}
});
useEffect(() => {
const initialProfile = async () => {
const result: any = await db.select(
`SELECT metadata FROM cache_profiles WHERE pubkey = "${pubkey}"`
);
db.close;
return result;
};
initialProfile()
.then((res) => {
if (res[0] !== undefined) {
setProfile(JSON.parse(res[0].metadata));
}
fetch(`https://rbr.bio/${pubkey}/metadata.json`).then((res) =>
res.json().then((res) => {
// update state
setProfile(JSON.parse(res.content));
})
.catch(console.error);
);
}, [pubkey]);
return (
<div className="relative flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
{profile.picture ? (
<ImageWithFallback
src={profile.picture}
alt={pubkey}
fill={true}
className="rounded-full object-cover"
/>
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
) : (
<Avatar
size={44}
name={pubkey}
variant="beam"
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
/>
<Avatar size={44} name={pubkey} variant="beam" colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']} />
)}
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full justify-between">
<div className="flex flex-col gap-1 text-sm">
<span className="font-bold leading-tight">
{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}
</span>
<span className="text-zinc-500">
{profile.username ? profile.username : truncate(pubkey, 16, ' .... ')}
</span>
<span className="font-bold leading-tight">{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}</span>
<span className="text-zinc-500">{profile.username ? profile.username : truncate(pubkey, 16, ' .... ')}</span>
</div>
<div>
<DotsHorizontalIcon className="h-4 w-4 text-zinc-500" />

View File

@ -0,0 +1,102 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
import { dateToUnix, hoursAgo } from '@utils/getDate';
import { ReloadIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
import { memo, useCallback, useContext, useEffect, useRef, useState } from 'react';
export const NoteConnector = memo(function NoteConnector({
setParentReload,
setHasNewNote,
currentDate,
}: {
setParentReload: any;
setHasNewNote: any;
currentDate: any;
}) {
const { db }: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const [follows]: any = useLocalStorage('follows');
const [relays]: any = useLocalStorage('relays');
const [reload, setReload] = useState(false);
const timeout = useRef(null);
const reloadNewsfeed = () => {
setParentReload(true);
setReload(true);
timeout.current = setTimeout(() => {
setReload(false);
}, 2000);
};
const insertDB = useCallback(
async (event: any) => {
await db.execute(
`INSERT OR IGNORE INTO
cache_notes
(id, pubkey, created_at, kind, tags, content) VALUES
("${event.id}", "${event.pubkey}", "${event.created_at}", "${event.kind}", '${JSON.stringify(event.tags)}', "${event.content}");`
);
},
[db]
);
const fetchEvent = useCallback(() => {
relayPool.subscribe(
[
{
kinds: [1],
authors: follows,
since: dateToUnix(hoursAgo(12, currentDate)),
},
],
relays,
(event: any) => {
// show trigger update newer event
if (event.created_at > dateToUnix(currentDate)) {
setHasNewNote(true);
}
// insert event to local database
insertDB(event).catch(console.error);
},
undefined,
(events: any, relayURL: any) => {
console.log(events, relayURL);
}
);
}, [relayPool, follows, currentDate, relays, insertDB, setHasNewNote]);
useEffect(() => {
fetchEvent();
return () => {
clearTimeout(timeout.current);
};
}, [fetchEvent]);
return (
<div className="relative flex h-12 items-center justify-between border-b border-zinc-800 px-6 shadow-input">
<div>
<h3 className="text-sm font-semibold text-zinc-500"># following</h3>
</div>
<div className="flex items-center gap-2">
<button onClick={() => reloadNewsfeed()} className={`${reload ? 'animate-spin' : ''} rounded-full p-1 hover:bg-zinc-800`}>
<ReloadIcon className="h-3.5 w-3.5 text-zinc-500" />
</button>
<div className="inline-flex items-center gap-1 rounded-full border border-zinc-700 bg-zinc-800 px-2.5 py-1">
{/* #TODO: get user network status */}
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-green-500"></span>
</span>
<p className="text-xs font-medium text-zinc-500">Online</p>
</div>
</div>
</div>
);
});

View File

@ -1,18 +1,33 @@
import { RelayContext } from '@components/contexts/relay';
import { Content } from '@components/note/content';
import NoteReply from '@components/note/modal/noteReply';
import { useNostrEvents } from 'nostr-react';
import { memo } from 'react';
import useLocalStorage from '@rehooks/local-storage';
import { memo, useContext, useState } from 'react';
/* eslint-disable @typescript-eslint/no-explicit-any */
const Modal = ({ event }: { event: any }) => {
const { events } = useNostrEvents({
filter: {
'#e': [event.id],
since: event.created_at,
kinds: [1],
const relayPool: any = useContext(RelayContext);
const [relays]: any = useLocalStorage('relays');
const [events, setEvents] = useState([]);
relayPool.subscribe(
[
{
'#e': [event.id],
since: event.created_at,
kinds: [1],
},
],
relays,
(event: any) => {
setEvents((events) => [event, ...events]);
},
});
undefined,
(events: any, relayURL: any) => {
console.log(events, relayURL);
}
);
return (
<div className="flex min-h-full items-center justify-center p-4">

View File

@ -1,49 +1,47 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { RelayContext } from '@components/contexts/relay';
import { UserRepost } from '@components/note/atoms/userRepost';
import { Content } from '@components/note/content';
import { Placeholder } from '@components/note/placeholder';
import * as Dialog from '@radix-ui/react-dialog';
import { LoopIcon } from '@radix-ui/react-icons';
import dynamic from 'next/dynamic';
import { useNostrEvents } from 'nostr-react';
import { memo } from 'react';
const Modal = dynamic(() => import('@components/note/modal'), {
ssr: false,
loading: () => <></>,
});
import useLocalStorage from '@rehooks/local-storage';
import { memo, useContext, useState } from 'react';
export const Repost = memo(function Repost({ root, user }: { root: any; user: string }) {
const { events } = useNostrEvents({
filter: {
ids: [root[0][1]],
since: 0,
kinds: [1],
const relayPool: any = useContext(RelayContext);
const [relays]: any = useLocalStorage('relays');
const [events, setEvents] = useState([]);
relayPool.subscribe(
[
{
ids: [root[0][1]],
since: 0,
kinds: [1],
},
],
relays,
(event: any) => {
setEvents((events) => [event, ...events]);
},
});
undefined,
(events: any, relayURL: any) => {
console.log(events, relayURL);
}
);
if (events !== null && Object.keys(events).length > 0) {
return (
<Dialog.Root>
<Dialog.Trigger>
<div className="flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-6 px-6">
<div className="flex items-center gap-1 pl-8 text-sm">
<LoopIcon className="h-4 w-4 text-zinc-400" />
<div className="ml-2">
<UserRepost pubkey={user} />
</div>
</div>
{events[0].content && <Content data={events[0]} />}
<div className="flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-6 px-6">
<div className="flex items-center gap-1 pl-8 text-sm">
<LoopIcon className="h-4 w-4 text-zinc-400" />
<div className="ml-2">
<UserRepost pubkey={user} />
</div>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
<Dialog.Content className="fixed inset-0 overflow-y-auto">
{events[0].content && <Modal event={events[0]} />}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Trigger>
</Dialog.Root>
</div>
{events[0].content && <Content data={events[0]} />}
</div>
);
} else {
return (

View File

@ -1,30 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Content } from '@components/note/content';
import * as Dialog from '@radix-ui/react-dialog';
import dynamic from 'next/dynamic';
import { memo } from 'react';
const Modal = dynamic(() => import('@components/note/modal'), {
ssr: false,
loading: () => <></>,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Single = memo(function Single({ event }: { event: any }) {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<div className="flex h-min min-h-min w-full cursor-pointer select-text flex-col border-b border-zinc-800 py-4 px-6 hover:bg-zinc-800">
<Content data={event} />
</div>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
<Dialog.Content className="fixed inset-0 overflow-y-auto">
<Modal event={event} />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<div className="flex h-min min-h-min w-full cursor-pointer select-text flex-col border-b border-zinc-800 py-4 px-6 hover:bg-zinc-800">
<Content data={event} />
</div>
);
});

View File

@ -1,53 +0,0 @@
import { Placeholder } from '@components/note/placeholder';
import { Repost } from '@components/note/repost';
import { Single } from '@components/note/single';
import { useCallback } from 'react';
import { Virtuoso } from 'react-virtuoso';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function Thread({ data }: { data: any }) {
const ItemContent = useCallback(
(index: string | number) => {
const event = data[index];
if (event.content.includes('#[0]') && event.tags[0][0] == 'e') {
// type: repost
return <Repost root={event.tags} user={event.pubkey} />;
} else {
// type: default
return <Single event={event} />;
}
},
[data]
);
const computeItemKey = useCallback(
(index) => {
return data[index].id;
},
[data]
);
return (
<Virtuoso
data={data}
itemContent={ItemContent}
components={{
EmptyPlaceholder: () => <Placeholder />,
ScrollSeekPlaceholder: () => <Placeholder />,
}}
computeItemKey={computeItemKey}
scrollSeekConfiguration={{
enter: (velocity) => Math.abs(velocity) > 800,
exit: (velocity) => Math.abs(velocity) < 500,
}}
overscan={800}
increaseViewportBy={1000}
className="scrollbar-hide relative h-full w-full rounded-lg"
style={{
contain: 'strict',
}}
/>
);
}

View File

@ -2,12 +2,10 @@
import AccountBar from '@components/accountBar';
import ActiveLink from '@components/activeLink';
import { currentUser } from '@stores/currentUser';
import { useStore } from '@nanostores/react';
import { useLocalStorage } from '@rehooks/local-storage';
export default function UserLayout({ children }: { children: React.ReactNode }) {
const $currentUser: any = useStore(currentUser);
const [currentUser]: any = useLocalStorage('current-user');
return (
<div className="flex h-full w-full flex-row">
@ -27,13 +25,13 @@ export default function UserLayout({ children }: { children: React.ReactNode })
</div>
<div className="flex flex-col gap-1 text-zinc-500">
<ActiveLink
href={`/profile/${$currentUser.pubkey}`}
href={`/profile/${currentUser.pubkey}`}
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900">
<span>Personal Page</span>
</ActiveLink>
<ActiveLink
href={`/profile/update?pubkey=${$currentUser.pubkey}`}
href={`/profile/update?pubkey=${currentUser.pubkey}`}
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900">
<span>Update Profile</span>

View File

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { relays } from '@stores/relays';
import DatabaseProvider from '@components/contexts/database';
import RelayProvider from '@components/contexts/relay';
import { useStore } from '@nanostores/react';
import { useLocalStorage } from '@rehooks/local-storage';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import { NostrProvider } from 'nostr-react';
import type { ReactElement, ReactNode } from 'react';
import { ReactElement, ReactNode } from 'react';
import '../App.css';
@ -21,12 +21,12 @@ type AppPropsWithLayout = AppProps & {
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
// Get relays
const $relays = useStore(relays);
// Get relays from localstorage
const [relays] = useLocalStorage('relays');
return (
<NostrProvider relayUrls={$relays} debug={false}>
{getLayout(<Component {...pageProps} />)}
</NostrProvider>
<DatabaseProvider>
<RelayProvider relays={relays}>{getLayout(<Component {...pageProps} />)}</RelayProvider>
</DatabaseProvider>
);
}

View File

@ -1,54 +1,113 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import NewsFeedLayout from '@layouts/newsfeedLayout';
import { DatabaseContext } from '@components/contexts/database';
import { NoteConnector } from '@components/note/connector';
import { Placeholder } from '@components/note/placeholder';
import { Thread } from '@components/thread';
import { Repost } from '@components/note/repost';
import { Single } from '@components/note/single';
import { hoursAgo } from '@utils/getDate';
import { dateToUnix } from '@utils/getDate';
import { follows } from '@stores/follows';
import { useStore } from '@nanostores/react';
import { dateToUnix, useNostrEvents } from 'nostr-react';
import {
JSXElementConstructor,
ReactElement,
ReactFragment,
ReactPortal,
Suspense,
useRef,
} from 'react';
import { writeStorage } from '@rehooks/local-storage';
import { useCallback, useState } from 'react';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useEffect, useRef } from 'react';
import { Virtuoso } from 'react-virtuoso';
export default function Page() {
const $follows = useStore(follows);
const now = useRef(new Date());
const { db }: any = useContext(DatabaseContext);
const { events } = useNostrEvents({
filter: {
authors: $follows,
since: dateToUnix(hoursAgo(6, now.current)),
kinds: [1],
limit: 100,
const [data, setData] = useState([]);
const [parentReload, setParentReload] = useState(false);
const [hasNewNote, setHasNewNote] = useState(false);
const now = useRef(new Date());
const limit = useRef(30);
const offset = useRef(0);
const loadMore = useCallback(async () => {
offset.current += limit.current;
// next query
const result = await db.select(
`SELECT * FROM
cache_notes
WHERE created_at <= ${dateToUnix(now.current)}
ORDER BY created_at DESC
LIMIT ${limit.current} OFFSET ${offset.current}`
);
setData((data) => [...data, ...result]);
}, [db]);
const ItemContent = useCallback(
(index: string | number) => {
const event = data[index];
if (event.content.includes('#[0]') && event.tags[0][0] == 'e') {
// type: repost
return <Repost root={event.tags} user={event.pubkey} />;
} else {
// type: default
return <Single event={event} />;
}
},
});
[data]
);
useEffect(() => {
const getData = async () => {
const result = await db.select(
`SELECT * FROM cache_notes WHERE created_at <= ${dateToUnix(now.current)} ORDER BY created_at DESC LIMIT ${limit.current}`
);
if (result) {
setData(result);
writeStorage('settings', new Date());
}
};
getData().catch(console.error);
}, [db, parentReload]);
const computeItemKey = useCallback(
(index: string | number) => {
return data[index].id;
},
[data]
);
return (
<div className="h-full w-full">
<Suspense fallback={<Placeholder />}>
<Thread data={events} />
</Suspense>
<div className="relative h-full w-full">
<NoteConnector setParentReload={setParentReload} setHasNewNote={setHasNewNote} currentDate={now.current} />
{hasNewNote && (
<div className="fixed top-10 left-1/2 z-50 -translate-x-1/2 transform">
<button>Load newest</button>
</div>
)}
<Virtuoso
data={data}
itemContent={ItemContent}
components={{
EmptyPlaceholder: () => <Placeholder />,
ScrollSeekPlaceholder: () => <Placeholder />,
}}
computeItemKey={computeItemKey}
scrollSeekConfiguration={{
enter: (velocity) => Math.abs(velocity) > 800,
exit: (velocity) => Math.abs(velocity) < 500,
}}
endReached={loadMore}
overscan={800}
increaseViewportBy={1000}
className="relative h-full w-full"
style={{
contain: 'strict',
}}
/>
</div>
);
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,50 +1,18 @@
import BaseLayout from '@layouts/baseLayout';
import NewsFeedLayout from '@layouts/newsfeedLayout';
import { Placeholder } from '@components/note/placeholder';
import { Thread } from '@components/thread';
import { hoursAgo } from '@utils/getDate';
import { dateToUnix, useNostrEvents } from 'nostr-react';
import {
JSXElementConstructor,
ReactElement,
ReactFragment,
ReactPortal,
Suspense,
useRef,
} from 'react';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
export default function Page() {
const now = useRef(new Date());
const { events } = useNostrEvents({
filter: {
until: dateToUnix(now.current),
since: dateToUnix(hoursAgo(1, now.current)),
kinds: [1],
limit: 10,
},
});
return (
<div className="h-full w-full">
<Suspense fallback={<Placeholder />}>
<Thread data={events} />
</Suspense>
<p>Global</p>
</div>
);
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -2,108 +2,31 @@
import BaseLayout from '@layouts/baseLayout';
import FullLayout from '@layouts/fullLayout';
import { currentUser } from '@stores/currentUser';
import { follows } from '@stores/follows';
import LumeSymbol from '@assets/icons/Lume';
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification';
import { useLocalStorage } from '@rehooks/local-storage';
import { motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
const db = typeof window !== 'undefined' ? await Database.load('sqlite:lume.db') : null;
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useEffect, useState } from 'react';
export default function Page() {
const router = useRouter();
const [currentUser]: any = useLocalStorage('current-user');
const [loading, setLoading] = useState(true);
const initDB = useCallback(async () => {
if (db) {
await db.execute(
'CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY, privkey TEXT NOT NULL, pubkey TEXT NOT NULL, npub TEXT, nsec TEXT, current INTEGER DEFAULT "0" NOT NULL, metadata JSON, UNIQUE(privkey));'
);
await db.execute('CREATE TABLE IF NOT EXISTS follows (id INTEGER PRIMARY KEY, pubkey TEXT NOT NULL, account TEXT, UNIQUE(pubkey));');
await db.execute(
'CREATE TABLE IF NOT EXISTS note_reactions (id INTEGER PRIMARY KEY, reaction_id TEXT NOT NULL, e TEXT, p TEXT, UNIQUE(reaction_id));'
);
await db.execute('CREATE TABLE IF NOT EXISTS note_replies (id INTEGER PRIMARY KEY, reply_id TEXT NOT NULL, e TEXT, p TEXT, UNIQUE(reply_id));');
await db.execute('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, event_id TEXT, event JSON, UNIQUE(event_id));');
await db.execute('CREATE TABLE IF NOT EXISTS cache_profiles (id INTEGER PRIMARY KEY, pubkey TEXT, metadata JSON, UNIQUE(pubkey));');
await db.execute('CREATE TABLE IF NOT EXISTS block_pubkeys (id INTEGER PRIMARY KEY, pubkey TEXT, UNIQUE(pubkey));');
await db.close();
}
}, []);
const notification = useCallback(async () => {
// NOTE: notification don't work in dev mode (only affect MacOS)
// ref: https://github.com/tauri-apps/tauri/issues/4965
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
const permission = await requestPermission();
permissionGranted = permission === 'granted';
}
if (permissionGranted) {
sendNotification({ title: 'Lume', body: 'Nostr is awesome' });
}
}, []);
const getAccount = useCallback(async () => {
const db = await Database.load('sqlite:lume.db');
const result = await db.select(`SELECT * FROM accounts WHERE current = "1" ORDER BY id ASC LIMIT 1`);
return result;
}, []);
const getFollows = useCallback(async (account) => {
const arr = [];
const db = await Database.load('sqlite:lume.db');
const result: any = await db.select(`SELECT pubkey FROM follows WHERE account = "${account.pubkey}"`);
result.forEach((item: { pubkey: string }) => {
arr.push(item.pubkey);
});
return arr;
}, []);
// Explain:
// Step 1: check DB tables, if not exist then create new table. #TODO: move this function to Rust code
// Step 2: request allow notification from system
// Step 3: get first account. #TODO: get last used account instead (part of multi account feature)
// Step 4: get follows by account
useEffect(() => {
initDB()
.then(() => notification())
.then(() => {
getAccount()
.then((res: any) => {
if (res.length === 0) {
setTimeout(() => {
setLoading(false);
router.push('/onboarding');
}, 1500);
} else {
// store current user in localstorage
currentUser.set(res[0]);
getFollows(res[0])
.then(async (res) => {
// store follows in localstorage
follows.set(res);
// redirect to newsfeed
setTimeout(() => {
setLoading(false);
router.push('/feed/following');
}, 1500);
})
.catch(console.error);
}
})
.catch(console.error);
})
.catch(console.error);
}, [getAccount, getFollows, initDB, notification, router]);
if (!currentUser) {
setTimeout(() => {
setLoading(false);
router.push('/onboarding');
}, 1500);
} else {
setTimeout(() => {
setLoading(false);
router.push('/feed/following');
}, 1500);
}
}, [currentUser, router]);
return (
<div className="relative flex h-full flex-col items-center justify-between">

View File

@ -2,107 +2,41 @@
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import { currentUser } from '@stores/currentUser';
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
import { useLocalStorage, writeStorage } from '@rehooks/local-storage';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { dateToUnix, useNostr } from 'nostr-react';
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useContext, useMemo, useState } from 'react';
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
const config: Config = {
dictionaries: [names],
};
const defaultAvatars = [
'https://bafybeidfsbrzqbvontmucteomoz2rkrxugu462l5hyhh6uioslkfzzs4oq.ipfs.w3s.link/avatar-11.png',
'https://bafybeid7mrvznbnd6r2ju2iu7lsxkcikufys6z6ssy5ldxrxq5qh3yqf4u.ipfs.w3s.link/avatar-12.png',
'https://bafybeih5gpwu53ohui6p7scekjpxjk2d4lusq2jqohqhjsvhfkeu56ea4e.ipfs.w3s.link/avatar-13.png',
'https://bafybeibpbvrpuphkerjygdbnh26av5brqggzunbbbmfl3ozlvcn2mj6zxa.ipfs.w3s.link/avatar-14.png',
'https://bafybeia4ue4loinuflu7y5q3xu6hcvt653mzw5yorw25oarf2wqksig4ma.ipfs.w3s.link/avatar-15.png',
'https://bafybeib3gzl6n2bebiru2cpkdljmlzbtqfsl6xcnqtabxt6jrpj7l7ltm4.ipfs.w3s.link/avatar-16.png',
];
const defaultBanners = [
'https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg',
'https://bafybeiderllqadxsikh3envikobmyka3uwgojriwh6epctqartq2loswyi.ipfs.w3s.link/banner-2.jpg',
'https://bafybeiba4tifde2kczvd26vxhbb5jpqi3wmgvccpkcrle4hse2cqrwlwiy.ipfs.w3s.link/banner-3.jpg',
'https://bafybeifqpny2eom7ccvmaguxxxfajutmn5h3fotaasga7gce2xfx37p6oy.ipfs.w3s.link/banner-4.jpg',
];
export default function Page() {
const router = useRouter();
const { publish } = useNostr();
const { db }: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const [relays] = useLocalStorage('relays');
const [type, setType] = useState('password');
const [loading, setLoading] = useState(false);
const [privKey] = useState(() => generatePrivateKey());
const [name] = useState(() => uniqueNamesGenerator(config).toString());
const [avatar] = useState(
() => defaultAvatars[Math.floor(Math.random() * defaultAvatars.length)]
);
const [banner] = useState(
() => defaultBanners[Math.floor(Math.random() * defaultBanners.length)]
);
const pubKey = getPublicKey(privKey);
const npub = nip19.npubEncode(pubKey);
const nsec = nip19.nsecEncode(privKey);
// auto-generated profile
const data = {
display_name: name,
name: name,
username: name.toLowerCase(),
picture: avatar,
banner: banner,
};
const createAccount = async () => {
setLoading(true);
// publish account to relays
const event: any = {
content: JSON.stringify(data),
created_at: dateToUnix(),
kind: 0,
pubkey: pubKey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, privKey);
publish(event);
// save account to database
const db = await Database.load('sqlite:lume.db');
await db.execute(
`INSERT INTO accounts (privkey, pubkey, npub, nsec, current, metadata) VALUES ("${privKey}", "${pubKey}", "${npub}", "${nsec}", "1", '${JSON.stringify(
data
)}')`
);
await db.close();
// set currentUser in global state
currentUser.set({
metadata: JSON.stringify(data),
npub: npub,
privkey: privKey,
pubkey: pubKey,
});
// redirect to pre-follow
setTimeout(() => {
setLoading(false);
router.push('/onboarding/following');
}, 1500);
};
const showNsec = () => {
const showPrivateKey = () => {
if (type === 'password') {
setType('text');
} else {
@ -110,19 +44,69 @@ export default function Page() {
}
};
// auto-generated profile
const data = useMemo(
() => ({
display_name: name,
name: name,
username: name.toLowerCase(),
picture: 'https://bafybeidfsbrzqbvontmucteomoz2rkrxugu462l5hyhh6uioslkfzzs4oq.ipfs.w3s.link/avatar-11.png',
banner: 'https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg',
}),
[name]
);
const insertDB = useCallback(async () => {
await db.execute(
`INSERT INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubKey}", "${privKey}", "${npub}", "${nsec}", '${JSON.stringify(data)}')`
);
}, [data, db, npub, nsec, privKey, pubKey]);
const createAccount = async () => {
setLoading(true);
// build event
const event: any = {
content: JSON.stringify(data),
created_at: Math.floor(Date.now() / 1000),
kind: 0,
pubkey: pubKey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, privKey);
insertDB()
.then(() => {
// publish to relays
relayPool.publish(event, relays);
// set currentUser in global state
writeStorage('current-user', {
metadata: JSON.stringify(data),
npub: npub,
privkey: privKey,
pubkey: pubKey,
});
// redirect to pre-follow
setTimeout(() => {
setLoading(false);
router.push('/onboarding/create/pre-follows');
}, 1500);
})
.catch(console.error);
};
return (
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Create new key
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
Lume will generate key with default profile for you, you can edit it later, and please
store your key safely so you can restore your account or use other client
Lume will generate key with default profile for you, you can edit it later, and please store your key safely so you can restore your
account or use other client
</motion.h2>
</div>
<div className="flex flex-col gap-4">
@ -146,7 +130,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
/>
<button
onClick={() => showNsec()}
onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700">
{type === 'password' ? (
<EyeClosedIcon className="h-5 w-5 text-zinc-500 group-hover:text-zinc-200" />
@ -157,19 +141,12 @@ export default function Page() {
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">
Default Profile (you can change it later)
</label>
<label className="text-sm font-semibold text-zinc-400">Default Profile (you can change it later)</label>
<div className="relative max-w-sm shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<div className="relative max-w-sm rounded-lg border border-black/5 px-3.5 py-4 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600">
<div className="flex space-x-4">
<div className="relative h-10 w-10 rounded-full">
<Image
className="inline-block rounded-full"
src={data.picture}
alt=""
fill={true}
/>
<Image className="inline-block rounded-full" src={data.picture} alt="" fill={true} />
</div>
<div className="flex-1 space-y-4 py-1">
<div className="flex items-center gap-2">
@ -193,18 +170,8 @@ export default function Page() {
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
@ -226,13 +193,7 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -2,29 +2,29 @@
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import { DatabaseContext } from '@components/contexts/database';
import { truncate } from '@utils/truncate';
import { currentUser } from '@stores/currentUser';
import data from '@assets/directory.json';
import { useStore } from '@nanostores/react';
import { CheckCircledIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { nip19 } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useState } from 'react';
const shuffle = (arr: { name: string; avatar: string; npub: string }[]) => [...arr].sort(() => Math.random() - 0.5);
export default function Page() {
const db: any = useContext(DatabaseContext);
const router = useRouter();
const shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5);
const [follow, setFollow] = useState([]);
const [loading, setLoading] = useState(false);
const [list] = useState(shuffle(data));
const $currentUser: any = useStore(currentUser);
const [currentUser]: any = useLocalStorage('current-user');
const followUser = (e) => {
const npub = e.currentTarget.getAttribute('data-npub');
@ -32,15 +32,12 @@ export default function Page() {
};
const insertDB = async () => {
const db = await Database.load('sqlite:lume.db');
await db.execute(
`INSERT INTO follows (pubkey, account) VALUES ("${$currentUser.pubkey}", "${$currentUser.pubkey}")`
);
// self follow
await db.execute(`INSERT INTO follows (pubkey, account, kind) VALUES ("${currentUser.pubkey}", "${currentUser.pubkey}", "0")`);
// follow selected
follow.forEach(async (npub) => {
const { data } = nip19.decode(npub);
await db.execute(
`INSERT INTO follows (pubkey, account) VALUES ("${data}", "${$currentUser.pubkey}")`
);
await db.execute(`INSERT INTO follows (pubkey, account, kind) VALUES ("${data}", "${currentUser.pubkey}", "0")`);
});
};
@ -60,14 +57,11 @@ export default function Page() {
<div>{/* spacer */}</div>
<motion.div layoutId="form" className="flex flex-col">
<div className="mb-8 flex flex-col gap-3">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Choose 10 people you want to following
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
For better experiences, you should follow the people you care about to personalize your
newsfeed, otherwise you will be very bored
For better experiences, you should follow the people you care about to personalize your newsfeed, otherwise you will be very bored
</motion.h2>
</div>
<div className="h-full w-full shrink">
@ -81,27 +75,14 @@ export default function Page() {
follow.includes(item.npub) ? 'bg-zinc-800' : ''
}`}>
<div className="relative h-10 w-10 flex-shrink-0">
<Image
className="rounded-full object-cover"
src={item.avatar}
alt={item.name}
fill={true}
/>
<Image className="rounded-full object-cover" src={item.avatar} alt={item.name} fill={true} />
</div>
<div className="inline-flex flex-1 items-center justify-between">
<div>
<p className="truncate text-sm font-medium text-zinc-200">{item.name}</p>
<p className="text-sm leading-tight text-zinc-500">
{truncate(item.npub, 16, ' .... ')}
</p>
</div>
<div>
{follow.includes(item.npub) ? (
<CheckCircledIcon className="h-4 w-4 text-green-500" />
) : (
<></>
)}
<p className="text-sm leading-tight text-zinc-500">{truncate(item.npub, 16, ' .... ')}</p>
</div>
<div>{follow.includes(item.npub) ? <CheckCircledIcon className="h-4 w-4 text-green-500" /> : <></>}</div>
</div>
</div>
))}
@ -111,18 +92,8 @@ export default function Page() {
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
@ -145,13 +116,7 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,125 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import { motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { useNostrEvents } from 'nostr-react';
import {
JSXElementConstructor,
ReactElement,
ReactFragment,
ReactPortal,
useEffect,
useState,
} from 'react';
import Database from 'tauri-plugin-sql-api';
export default function Page() {
const [follows, setFollows] = useState([null]);
const [loading, setLoading] = useState(false);
const router = useRouter();
const { pubkey }: any = router.query;
const { onEvent } = useNostrEvents({
filter: {
authors: [pubkey],
kinds: [3],
},
});
onEvent((rawMetadata) => {
try {
setFollows(rawMetadata.tags);
} catch (err) {
console.error(err, rawMetadata);
}
});
useEffect(() => {
setLoading(true);
const insertDB = async () => {
const db = await Database.load('sqlite:lume.db');
follows.forEach(async (item) => {
if (item) {
await db.execute(
`INSERT OR IGNORE INTO follows (pubkey, account) VALUES ("${item[1]}", "${pubkey}")`
);
}
});
};
if (follows !== null && follows.length > 0) {
insertDB()
.then(() => {
setTimeout(() => {
setLoading(false);
router.push('/');
}, 1500);
})
.catch(console.error);
}
}, [follows, pubkey, router]);
return (
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Fetching your follows...
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
Not only profile, every nostr client can sync your follows list when you move to a new
client, so please keep your key safely (again)
</motion.h2>
</div>
</motion.div>
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<></>
)}
</div>
</motion.div>
</div>
);
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>
<OnboardingLayout>{page}</OnboardingLayout>
</BaseLayout>
);
};

View File

@ -1,134 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import { motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { useNostrEvents } from 'nostr-react';
import { getPublicKey, nip19 } from 'nostr-tools';
import {
JSXElementConstructor,
ReactElement,
ReactFragment,
ReactPortal,
useEffect,
useState,
} from 'react';
import Database from 'tauri-plugin-sql-api';
export default function Page() {
const router = useRouter();
const { privkey }: any = router.query;
const [account, setAccount] = useState(null);
const [loading, setLoading] = useState(false);
const pubkey = privkey ? getPublicKey(privkey) : null;
const npub = privkey ? nip19.npubEncode(pubkey) : null;
const nsec = privkey ? nip19.nsecEncode(privkey) : null;
const { onEvent } = useNostrEvents({
filter: {
authors: [pubkey],
kinds: [0],
},
});
onEvent((rawMetadata) => {
try {
const metadata: any = JSON.parse(rawMetadata.content);
setAccount(metadata);
} catch (err) {
console.error(err, rawMetadata);
}
});
useEffect(() => {
setLoading(true);
const insertDB = async () => {
// save account to database
const db = await Database.load('sqlite:lume.db');
await db.execute(
`INSERT INTO accounts (privkey, pubkey, npub, nsec, current, metadata) VALUES ("${privkey}", "${pubkey}", "${npub}", "${nsec}", "1", '${JSON.stringify(
account
)}')`
);
await db.close();
};
if (account !== null) {
insertDB()
.then(() => {
setTimeout(() => {
setLoading(false);
router.push({
pathname: '/onboarding/fetch-follows',
query: { pubkey: pubkey },
});
}, 1500);
})
.catch(console.error);
}
}, [account, npub, nsec, privkey, pubkey, router]);
return (
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Fetching your profile...
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
As long as you have private key, you alway can sync your profile on every nostr client,
so please keep your key safely
</motion.h2>
</div>
</motion.div>
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<></>
)}
</div>
</motion.div>
</div>
);
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>
<OnboardingLayout>{page}</OnboardingLayout>
</BaseLayout>
);
};

View File

@ -10,9 +10,7 @@ export default function Page() {
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<div className="flex flex-col gap-3">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Other social network require email/password
<br />
nostr use{' '}
@ -21,8 +19,8 @@ export default function Page() {
</span>
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
If you have used nostr before, you can import your own private key. Otherwise, you can
create a new key or use auto-generated account created by system.
If you have used nostr before, you can import your own private key. Otherwise, you can create a new key or use auto-generated account
created by system.
</motion.h2>
<motion.div layoutId="form"></motion.div>
<motion.div layoutId="action" className="mt-4 flex gap-2">
@ -32,7 +30,7 @@ export default function Page() {
Create new key
</Link>
<Link
href="/onboarding/import"
href="/onboarding/login"
className="hover:bg-zinc-900/2.5 transform rounded-lg border border-black/5 bg-zinc-800 px-3.5 py-2 font-medium ring-1 ring-inset ring-zinc-900/10 hover:text-zinc-900 active:translate-y-1 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-zinc-700 dark:hover:text-white">
Login with private key
</Link>
@ -44,13 +42,7 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -0,0 +1,122 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
import { useLocalStorage } from '@rehooks/local-storage';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { getPublicKey, nip19 } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useContext, useMemo, useState } from 'react';
export default function Page() {
const { db }: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const [loading, setLoading] = useState(false);
const [relays] = useLocalStorage('relays');
const router = useRouter();
const { privkey }: any = router.query;
const pubkey = useMemo(() => (privkey ? getPublicKey(privkey) : null), [privkey]);
// save account to database
const insertAccount = useCallback(
async (metadata) => {
if (loading === false) {
const npub = privkey ? nip19.npubEncode(pubkey) : null;
const nsec = privkey ? nip19.nsecEncode(privkey) : null;
await db.execute(
`INSERT OR IGNORE INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubkey}", "${privkey}", "${npub}", "${nsec}", '${metadata}')`
);
setLoading(true);
}
},
[db, privkey, pubkey, loading]
);
// save follows to database
const insertFollows = useCallback(
async (follows) => {
follows.forEach(async (item) => {
if (item) {
await db.execute(`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${item[1]}", "${pubkey}", "0")`);
}
});
},
[db, pubkey]
);
relayPool.subscribe(
[
{
authors: [pubkey],
kinds: [0, 3],
since: 0,
},
],
relays,
(event: any) => {
if (event.kind === 0) {
insertAccount(event.content);
} else {
if (event.tags.length > 0) {
insertFollows(event.tags);
}
}
},
undefined,
(events: any, relayURL: any) => {
console.log(events, relayURL);
}
);
return (
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Fetching your profile...
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
As long as you have private key, you alway can sync your profile and follows list on every nostr client, so please keep your key safely
</motion.h2>
</div>
</motion.div>
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<Link
href="/"
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30">
<span className="drop-shadow-lg">Finish</span>
</Link>
)}
</div>
</motion.div>
</div>
);
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>
<OnboardingLayout>{page}</OnboardingLayout>
</BaseLayout>
);
};

View File

@ -44,7 +44,7 @@ export default function Page() {
try {
router.push({
pathname: '/onboarding/fetch-profile',
pathname: '/onboarding/login/fetch',
query: { privkey: privkey },
});
} catch (error) {
@ -60,14 +60,12 @@ export default function Page() {
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Import your private key
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
You can import private key format as hex string or nsec. If you have installed Nostr
Connect compality wallet in your mobile, you can connect by scan QR Code below
You can import private key format as hex string or nsec. If you have installed Nostr Connect compality wallet in your mobile, you can
connect by scan QR Code below
</motion.h2>
</div>
<div className="flex flex-col gap-2">
@ -85,18 +83,8 @@ export default function Page() {
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{isSubmitting ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
@ -117,13 +105,7 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -2,13 +2,14 @@
import BaseLayout from '@layouts/baseLayout';
import UserLayout from '@layouts/userLayout';
import { currentUser } from '@stores/currentUser';
import { RelayContext } from '@components/contexts/relay';
import { useStore } from '@nanostores/react';
import { dateToUnix } from '@utils/getDate';
import { useLocalStorage } from '@rehooks/local-storage';
import { useRouter } from 'next/router';
import { dateToUnix, useNostr } from 'nostr-react';
import { getEventHash, signEvent } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useState } from 'react';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useState } from 'react';
import { useForm } from 'react-hook-form';
import Database from 'tauri-plugin-sql-api';
@ -24,15 +25,14 @@ type FormValues = {
// TODO: update the design
export default function Page() {
const relayPool: any = useContext(RelayContext);
const [relays]: any = useLocalStorage('relays');
const router = useRouter();
const { publish } = useNostr();
const [loading, setLoading] = useState(false);
const $currentUser: any = useStore(currentUser);
const profile =
$currentUser.metadata !== undefined
? JSON.parse($currentUser.metadata)
: { display_name: null, username: null };
const [currentUser]: any = useLocalStorage('current-user');
const profile = currentUser.metadata !== undefined ? JSON.parse(currentUser.metadata) : { display_name: null, username: null };
const {
register,
@ -48,28 +48,25 @@ export default function Page() {
content: JSON.stringify(data),
created_at: dateToUnix(),
kind: 0,
pubkey: $currentUser.pubkey,
pubkey: currentUser.pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, $currentUser.privkey);
publish(event);
event.sig = signEvent(event, currentUser.privkey);
relayPool.publish(event, relays);
// save account to database
const db = await Database.load('sqlite:lume.db');
await db.execute(
`UPDATE accounts SET metadata = '${JSON.stringify(data)}' WHERE pubkey = "${
$currentUser.pubkey
}"`
);
await db.close();
await db.execute(`UPDATE accounts SET metadata = '${JSON.stringify(data)}' WHERE pubkey = "${currentUser.pubkey}"`);
// set currentUser in global state
currentUser.set({
metadata: JSON.stringify(data),
npub: $currentUser.npub,
privkey: $currentUser.privkey,
pubkey: $currentUser.pubkey,
npub: currentUser.npub,
privkey: currentUser.privkey,
pubkey: currentUser.pubkey,
});
// redirect to newsfeed
@ -80,16 +77,11 @@ export default function Page() {
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col justify-between px-6">
<form onSubmit={handleSubmit(onSubmit)} className="flex h-full w-full flex-col justify-between px-6">
<div className="mb-8 flex flex-col gap-3 pt-8">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Update profile
</h1>
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">Update profile</h1>
<h2 className="w-3/4 text-zinc-400">
Your profile will be published to all relays, as long as you have the private key, you
always can recover your profile in any client
Your profile will be published to all relays, as long as you have the private key, you always can recover your profile in any client
</h2>
</div>
<fieldset className="flex flex-col gap-2">
@ -105,9 +97,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<span className="text-sm text-red-400">
{errors.display_name && <p>{errors.display_name.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.display_name && <p>{errors.display_name.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -122,9 +112,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<span className="text-sm text-red-400">
{errors.name && <p>{errors.name.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.name && <p>{errors.name.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -139,9 +127,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<span className="text-sm text-red-400">
{errors.username && <p>{errors.username.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.username && <p>{errors.username.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -156,9 +142,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<span className="text-sm text-red-400">
{errors.picture && <p>{errors.picture.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.picture && <p>{errors.picture.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -173,9 +157,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<span className="text-sm text-red-400">
{errors.banner && <p>{errors.banner.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.banner && <p>{errors.banner.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -190,27 +172,15 @@ export default function Page() {
className="relative h-24 w-full resize-none rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<span className="text-sm text-red-400">
{errors.about && <p>{errors.about.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.about && <p>{errors.about.message}</p>}</span>
</div>
</div>
</fieldset>
<div className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
@ -231,13 +201,7 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,10 +0,0 @@
import { persistentAtom } from '@nanostores/persistent';
export const currentUser = persistentAtom(
'currentUser',
{},
{
encode: JSON.stringify,
decode: JSON.parse,
}
);

View File

@ -1,10 +0,0 @@
import { persistentAtom } from '@nanostores/persistent';
export const follows = persistentAtom('follows', [], {
encode(value) {
return JSON.stringify(value);
},
decode(value) {
return JSON.parse(value);
},
});

View File

@ -1,25 +0,0 @@
import { persistentAtom } from '@nanostores/persistent';
export const relays = persistentAtom(
'relays',
[
'wss://relay.uselume.xyz',
'wss://nostr-pub.wellorder.net',
'wss://nostr.bongbong.com',
'wss://nostr.zebedee.cloud',
'wss://nostr.fmt.wiz.biz',
'wss://nostr.walletofsatoshi.com',
'wss://relay.snort.social',
'wss://offchain.pub',
'wss://nos.lol',
'wss://relay.damus.io',
],
{
encode(value) {
return JSON.stringify(value);
},
decode(value) {
return JSON.parse(value);
},
}
);

View File

@ -11,3 +11,9 @@ export const hoursAgo = (numOfHours, date = new Date()) => {
return hoursAgo;
};
export const dateToUnix = (_date?: Date) => {
const date = _date || new Date();
return Math.floor(date.getTime() / 1000);
};

View File

@ -6,7 +6,6 @@
"@layouts/*": ["src/layouts/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@stores/*": ["src/stores/*"],
"@assets/*": ["src/assets/*"]
},
"target": "es2017",