mirror of
https://github.com/lumehq/lume.git
synced 2025-03-26 01:31:48 +01:00
update channel
This commit is contained in:
parent
a71502d19e
commit
87e8ee8954
14
package.json
14
package.json
@ -25,13 +25,13 @@
|
||||
"destr": "^1.2.2",
|
||||
"iconoir-react": "^6.6.0",
|
||||
"jotai": "^2.0.4",
|
||||
"nostr-relaypool": "^0.5.18",
|
||||
"nostr-relaypool": "^0.6.27",
|
||||
"nostr-tools": "^1.10.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-string-replace": "^1.1.0",
|
||||
"react-virtuoso": "^4.3.1",
|
||||
"react-virtuoso": "^4.3.2",
|
||||
"react-youtube": "^10.1.0",
|
||||
"swr": "^2.1.5",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
@ -42,7 +42,7 @@
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tauri-apps/cli": "^1.2.3",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"@types/node": "^18.16.1",
|
||||
"@types/node": "^18.16.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/youtube-player": "^5.5.7",
|
||||
@ -57,17 +57,17 @@
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.3.4",
|
||||
"eslint-plugin-react-refresh": "^0.3.5",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.1",
|
||||
"lint-staged": "^13.2.2",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.2.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.3.2",
|
||||
"vite-plugin-ssr": "^0.4.117",
|
||||
"vite": "^4.3.3",
|
||||
"vite-plugin-ssr": "^0.4.118",
|
||||
"vite-plugin-top-level-await": "^1.3.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"ws": "^8.13.0"
|
||||
|
112
pnpm-lock.yaml
generated
112
pnpm-lock.yaml
generated
@ -35,8 +35,8 @@ dependencies:
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4(react@18.2.0)
|
||||
nostr-relaypool:
|
||||
specifier: ^0.5.18
|
||||
version: 0.5.18(ws@8.13.0)
|
||||
specifier: ^0.6.27
|
||||
version: 0.6.27(ws@8.13.0)
|
||||
nostr-tools:
|
||||
specifier: ^1.10.1
|
||||
version: 1.10.1
|
||||
@ -53,8 +53,8 @@ dependencies:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
react-virtuoso:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1(react-dom@18.2.0)(react@18.2.0)
|
||||
specifier: ^4.3.2
|
||||
version: 4.3.2(react-dom@18.2.0)(react@18.2.0)
|
||||
react-youtube:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(react@18.2.0)
|
||||
@ -82,8 +82,8 @@ devDependencies:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1(prettier@2.8.8)
|
||||
'@types/node':
|
||||
specifier: ^18.16.1
|
||||
version: 18.16.1
|
||||
specifier: ^18.16.2
|
||||
version: 18.16.2
|
||||
'@types/react':
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
@ -101,7 +101,7 @@ devDependencies:
|
||||
version: 5.59.1(eslint@8.39.0)(typescript@4.9.5)
|
||||
'@vitejs/plugin-react-swc':
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0(vite@4.3.2)
|
||||
version: 3.3.0(vite@4.3.3)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.14
|
||||
version: 10.4.14(postcss@8.4.23)
|
||||
@ -127,14 +127,14 @@ devDependencies:
|
||||
specifier: ^4.6.0
|
||||
version: 4.6.0(eslint@8.39.0)
|
||||
eslint-plugin-react-refresh:
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(eslint@8.39.0)
|
||||
specifier: ^0.3.5
|
||||
version: 0.3.5(eslint@8.39.0)
|
||||
husky:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3
|
||||
lint-staged:
|
||||
specifier: ^13.2.1
|
||||
version: 13.2.1
|
||||
specifier: ^13.2.2
|
||||
version: 13.2.2
|
||||
postcss:
|
||||
specifier: ^8.4.23
|
||||
version: 8.4.23
|
||||
@ -154,17 +154,17 @@ devDependencies:
|
||||
specifier: ^4.9.5
|
||||
version: 4.9.5
|
||||
vite:
|
||||
specifier: ^4.3.2
|
||||
version: 4.3.2(@types/node@18.16.1)
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(@types/node@18.16.2)
|
||||
vite-plugin-ssr:
|
||||
specifier: ^0.4.117
|
||||
version: 0.4.117(vite@4.3.2)
|
||||
specifier: ^0.4.118
|
||||
version: 0.4.118(vite@4.3.3)
|
||||
vite-plugin-top-level-await:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0(vite@4.3.2)
|
||||
version: 1.3.0(vite@4.3.3)
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0(typescript@4.9.5)(vite@4.3.2)
|
||||
version: 4.2.0(typescript@4.9.5)(vite@4.3.3)
|
||||
ws:
|
||||
specifier: ^8.13.0
|
||||
version: 8.13.0
|
||||
@ -587,9 +587,9 @@ packages:
|
||||
{ integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg== }
|
||||
dev: false
|
||||
|
||||
/@floating-ui/dom@1.2.6:
|
||||
/@floating-ui/dom@1.2.7:
|
||||
resolution:
|
||||
{ integrity: sha512-02vxFDuvuVPs22iJICacezYJyf7zwwOCWkPNkWNBr1U0Qt1cKFYzWvxts0AmqcOQGwt/3KJWcWIgtbUU38keyw== }
|
||||
{ integrity: sha512-DyqylONj1ZaBnzj+uBnVfzdjjCkFCL2aA9ESHLyUOGSqb03RpbLMImP1ekIQXYs4KLk9jAjJfZAU8hXfWSahEg== }
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.2.6
|
||||
dev: false
|
||||
@ -601,7 +601,7 @@ packages:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.2.6
|
||||
'@floating-ui/dom': 1.2.7
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
@ -703,9 +703,9 @@ packages:
|
||||
'@jridgewell/resolve-uri': 3.1.0
|
||||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
|
||||
/@maverick-js/signals@5.9.3:
|
||||
/@maverick-js/signals@5.9.4:
|
||||
resolution:
|
||||
{ integrity: sha512-rqaaetjcqQQXbloejGYyHqN6i+cf2Lp88nw8qx2s86CD0X+1Tl/dq+I53wFM6VK6cvm925fQLszGG24AMSWAaw== }
|
||||
{ integrity: sha512-NJizvl2Pk0pWOjB+h0u2k+9pbdcF/zQf3Msohc79cldwhHXbWoPakIAQphrRdxtLUHBM1hm3ZJwVndI+on0+Zg== }
|
||||
dev: false
|
||||
|
||||
/@noble/hashes@1.2.0:
|
||||
@ -789,9 +789,9 @@ packages:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/@supabase/gotrue-js@2.23.0(encoding@0.1.13):
|
||||
/@supabase/gotrue-js@2.24.0(encoding@0.1.13):
|
||||
resolution:
|
||||
{ integrity: sha512-N6o+MMGsPDbdiz0R0Oy9GlgefYFjJJvoBduR16s8c1H3yG2jp6jq+pMEP18P1bg7uk2DljEjyBnVN7Wj7SJ2Zw== }
|
||||
{ integrity: sha512-ZsH4K5cbMTjfMytXaDYVYs9l9igmlZFxiwXn7J2IP/CklWR5qmLCma+dvat5rccPLITVkN6oAZbKxDzW+pEgCg== }
|
||||
dependencies:
|
||||
cross-fetch: 3.1.5(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
@ -832,7 +832,7 @@ packages:
|
||||
{ integrity: sha512-FW3ZzBoc4orSgfX0dXrmJoXAcI/hiekmqXTkN64vjtUF2Urp3UjyAf71UTtV9Jl6ejHoe3K++e0+Rg9zKUJh5w== }
|
||||
dependencies:
|
||||
'@supabase/functions-js': 2.1.1(encoding@0.1.13)
|
||||
'@supabase/gotrue-js': 2.23.0(encoding@0.1.13)
|
||||
'@supabase/gotrue-js': 2.24.0(encoding@0.1.13)
|
||||
'@supabase/postgrest-js': 1.6.0(encoding@0.1.13)
|
||||
'@supabase/realtime-js': 2.7.2
|
||||
'@supabase/storage-js': 2.5.1(encoding@0.1.13)
|
||||
@ -1156,9 +1156,9 @@ packages:
|
||||
{ integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== }
|
||||
dev: true
|
||||
|
||||
/@types/node@18.16.1:
|
||||
/@types/node@18.16.2:
|
||||
resolution:
|
||||
{ integrity: sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA== }
|
||||
{ integrity: sha512-GQW/JL/5Fz/0I8RpeBG9lKp0+aNcXEaVL71c0D2Q0QHDTFvlYKT7an0onCUXj85anv7b4/WesqdfchLc0jtsCg== }
|
||||
|
||||
/@types/phoenix@1.5.6:
|
||||
resolution:
|
||||
@ -1197,7 +1197,7 @@ packages:
|
||||
resolution:
|
||||
{ integrity: sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ== }
|
||||
dependencies:
|
||||
'@types/node': 18.16.1
|
||||
'@types/node': 18.16.2
|
||||
dev: false
|
||||
|
||||
/@types/youtube-player@5.5.7:
|
||||
@ -1361,14 +1361,14 @@ packages:
|
||||
vidstack: 0.4.5
|
||||
dev: false
|
||||
|
||||
/@vitejs/plugin-react-swc@3.3.0(vite@4.3.2):
|
||||
/@vitejs/plugin-react-swc@3.3.0(vite@4.3.3):
|
||||
resolution:
|
||||
{ integrity: sha512-Ycg+n2eyCOTpn/wRy+evVo859+hw7qCj9iaX5CMny6x1fx1Uoq0xBG+a98lFtwLNGfGEnpI0F26YigRuxCRkwg== }
|
||||
peerDependencies:
|
||||
vite: ^4
|
||||
dependencies:
|
||||
'@swc/core': 1.3.55
|
||||
vite: 4.3.2(@types/node@18.16.1)
|
||||
vite: 4.3.3(@types/node@18.16.2)
|
||||
transitivePeerDependencies:
|
||||
- '@swc/helpers'
|
||||
dev: true
|
||||
@ -1593,7 +1593,7 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001481
|
||||
electron-to-chromium: 1.4.372
|
||||
electron-to-chromium: 1.4.376
|
||||
node-releases: 2.0.10
|
||||
update-browserslist-db: 1.0.11(browserslist@4.21.5)
|
||||
dev: true
|
||||
@ -1897,9 +1897,9 @@ packages:
|
||||
{ integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== }
|
||||
dev: true
|
||||
|
||||
/electron-to-chromium@1.4.372:
|
||||
/electron-to-chromium@1.4.376:
|
||||
resolution:
|
||||
{ integrity: sha512-MrlFq/j+TYHOjeWsWGYfzevc25HNeJdsF6qaLFrqBTRWZQtWkb1myq/Q2veLWezVaa5OcSZ99CFwTT4aF4Mung== }
|
||||
{ integrity: sha512-TFeOKd98TpJzRHkr4Aorn16QkMnuCQuGAE6IZ0wYF+qkbSfMPqjplvRppR02tMUpVxZz8nyBNvVm9lIZsqrbPQ== }
|
||||
dev: true
|
||||
|
||||
/emoji-regex@8.0.0:
|
||||
@ -2088,9 +2088,9 @@ packages:
|
||||
eslint: 8.39.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-react-refresh@0.3.4(eslint@8.39.0):
|
||||
/eslint-plugin-react-refresh@0.3.5(eslint@8.39.0):
|
||||
resolution:
|
||||
{ integrity: sha512-E0ViBglxSQAERBp6eTj5fPgtCRtDonnbCFiVQBhf4Dto2blJRxg1dFUMdMh7N6ljTI4UwPhHwYDQ3Dyo4m6bwA== }
|
||||
{ integrity: sha512-61qNIsc7fo9Pp/mju0J83kzvLm0Bsayu7OQSLEoJxLDCBjIIyb87bkzufoOvdDxLkSlMfkF7UxomC4+eztUBSA== }
|
||||
peerDependencies:
|
||||
eslint: '>=7'
|
||||
dependencies:
|
||||
@ -2903,9 +2903,9 @@ packages:
|
||||
{ integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== }
|
||||
dev: true
|
||||
|
||||
/lint-staged@13.2.1:
|
||||
/lint-staged@13.2.2:
|
||||
resolution:
|
||||
{ integrity: sha512-8gfzinVXoPfga5Dz/ZOn8I2GOhf81Wvs+KwbEXQn/oWZAvCVS2PivrXfVbFJc93zD16uC0neS47RXHIjXKYZQw== }
|
||||
{ integrity: sha512-71gSwXKy649VrSU09s10uAT0rWCcY3aewhMaHyl2N84oBk4Xs9HgxvUp3AYu+bNsK4NrOYYxvSgg7FyGJ+jGcA== }
|
||||
engines: { node: ^14.13.1 || >=16.0.0 }
|
||||
hasBin: true
|
||||
dependencies:
|
||||
@ -2942,7 +2942,7 @@ packages:
|
||||
log-update: 4.0.0
|
||||
p-map: 4.0.0
|
||||
rfdc: 1.3.0
|
||||
rxjs: 7.8.0
|
||||
rxjs: 7.8.1
|
||||
through: 2.3.8
|
||||
wrap-ansi: 7.0.0
|
||||
dev: true
|
||||
@ -3011,7 +3011,7 @@ packages:
|
||||
{ integrity: sha512-p8L5V62CV6TmHAngmRAopp231oJKeH77mJja5SsKOfvzrPRoThT/Jo9U0jMRB5iMykqkvyg2J5V5Agn6FPXDWQ== }
|
||||
engines: { node: '>=16' }
|
||||
dependencies:
|
||||
'@maverick-js/signals': 5.9.3
|
||||
'@maverick-js/signals': 5.9.4
|
||||
type-fest: 3.9.0
|
||||
dev: false
|
||||
|
||||
@ -3150,9 +3150,9 @@ packages:
|
||||
engines: { node: '>=0.10.0' }
|
||||
dev: true
|
||||
|
||||
/nostr-relaypool@0.5.18(ws@8.13.0):
|
||||
/nostr-relaypool@0.6.27(ws@8.13.0):
|
||||
resolution:
|
||||
{ integrity: sha512-l3bR034akVSdd/jSOqcj33k1BYaAwqbJMJQFg3cJ/BXPrXy6fPSd4Qye0vuF+MwtqokmLFa68MM8hbfHeOgYtQ== }
|
||||
{ integrity: sha512-YtQxb8z9VHsPEQfC4rkxztqyGvWM1kcwiLhp/N8PpZX1+9mJhoIFctgpGxWB1LXhZgRiyJfY5Ml4EklvtWELuw== }
|
||||
dependencies:
|
||||
'@jest/source-map': 29.4.3
|
||||
isomorphic-ws: 5.0.0(ws@8.13.0)
|
||||
@ -3591,9 +3591,9 @@ packages:
|
||||
engines: { node: '>=0.12.0' }
|
||||
dev: false
|
||||
|
||||
/react-virtuoso@4.3.1(react-dom@18.2.0)(react@18.2.0):
|
||||
/react-virtuoso@4.3.2(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution:
|
||||
{ integrity: sha512-2+V0bvA1fASO+etlBG6YB0uj+StizxP3ecDJXgGW/r2z9AH067ehpJy2TSRiEIGQtDTmJAcmZnZzYVnk7AUmbw== }
|
||||
{ integrity: sha512-n51V4fMk36VM6NapfaMbfs4w/IaVodp5asp+rtzlvz0yurG6uVrqA040ke43gpYtoOL6j6TxeXWo4b6GpO9N0A== }
|
||||
engines: { node: '>=10' }
|
||||
peerDependencies:
|
||||
react: '>=16 || >=17 || >= 18'
|
||||
@ -3721,9 +3721,9 @@ packages:
|
||||
queue-microtask: 1.2.3
|
||||
dev: true
|
||||
|
||||
/rxjs@7.8.0:
|
||||
/rxjs@7.8.1:
|
||||
resolution:
|
||||
{ integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== }
|
||||
{ integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== }
|
||||
dependencies:
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
@ -4262,14 +4262,14 @@ packages:
|
||||
type-fest: 3.9.0
|
||||
dev: false
|
||||
|
||||
/vite-plugin-ssr@0.4.117(vite@4.3.2):
|
||||
/vite-plugin-ssr@0.4.118(vite@4.3.3):
|
||||
resolution:
|
||||
{ integrity: sha512-jebuK9HGNTpwbNlhIygTmNX8W9ZJTClUv/V5MfvRbVGqL0jGkYbsKKwzRNbUDm5EMqblbFf9mMHt6KSIuh/YJQ== }
|
||||
{ integrity: sha512-L1t/4eGraa9wBfkt/EHvTm1l89CVCS1So9XfR7XfufA27WNxpRDD/EkYbd1pOIYMrE3aONNAQEZakVVzu0Xd1g== }
|
||||
engines: { node: '>=12.19.0' }
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
react-streaming: '>=0.3.5'
|
||||
vite: '>=3.0.0'
|
||||
vite: '>=3.1.0'
|
||||
peerDependenciesMeta:
|
||||
react-streaming:
|
||||
optional: true
|
||||
@ -4284,10 +4284,10 @@ packages:
|
||||
fast-glob: 3.2.12
|
||||
picocolors: 1.0.0
|
||||
sirv: 2.0.3
|
||||
vite: 4.3.2(@types/node@18.16.1)
|
||||
vite: 4.3.3(@types/node@18.16.2)
|
||||
dev: true
|
||||
|
||||
/vite-plugin-top-level-await@1.3.0(vite@4.3.2):
|
||||
/vite-plugin-top-level-await@1.3.0(vite@4.3.3):
|
||||
resolution:
|
||||
{ integrity: sha512-owIfsgWudMlQODWJSwp0sQB3AZZu3qsMygeBjZy8CyjEk6OB9AGd8lHqmgwrcEqgvy9N58lYxSBLVk3/4ejEiA== }
|
||||
peerDependencies:
|
||||
@ -4296,13 +4296,13 @@ packages:
|
||||
'@rollup/plugin-virtual': 3.0.1
|
||||
'@swc/core': 1.3.55
|
||||
uuid: 9.0.0
|
||||
vite: 4.3.2(@types/node@18.16.1)
|
||||
vite: 4.3.3(@types/node@18.16.2)
|
||||
transitivePeerDependencies:
|
||||
- '@swc/helpers'
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/vite-tsconfig-paths@4.2.0(typescript@4.9.5)(vite@4.3.2):
|
||||
/vite-tsconfig-paths@4.2.0(typescript@4.9.5)(vite@4.3.3):
|
||||
resolution:
|
||||
{ integrity: sha512-jGpus0eUy5qbbMVGiTxCL1iB9ZGN6Bd37VGLJU39kTDD6ZfULTTb1bcc5IeTWqWJKiWV5YihCaibeASPiGi8kw== }
|
||||
peerDependencies:
|
||||
@ -4314,15 +4314,15 @@ packages:
|
||||
debug: 4.3.4
|
||||
globrex: 0.1.2
|
||||
tsconfck: 2.1.1(typescript@4.9.5)
|
||||
vite: 4.3.2(@types/node@18.16.1)
|
||||
vite: 4.3.3(@types/node@18.16.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/vite@4.3.2(@types/node@18.16.1):
|
||||
/vite@4.3.3(@types/node@18.16.2):
|
||||
resolution:
|
||||
{ integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw== }
|
||||
{ integrity: sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA== }
|
||||
engines: { node: ^14.18.0 || >=16.0.0 }
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@ -4346,7 +4346,7 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.16.1
|
||||
'@types/node': 18.16.2
|
||||
esbuild: 0.17.18
|
||||
postcss: 8.4.23
|
||||
rollup: 3.21.0
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { UserMuted } from '@lume/shared/user/muted';
|
||||
import MutedItem from '@lume/app/channel/components/mutedItem';
|
||||
|
||||
import { Popover, Transition } from '@headlessui/react';
|
||||
import { MicMute } from 'iconoir-react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
export const ChannelBlackList = ({ blacklist }: { blacklist: any }) => {
|
||||
export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
@ -39,7 +39,7 @@ export const ChannelBlackList = ({ blacklist }: { blacklist: any }) => {
|
||||
</div>
|
||||
<div className="px-3 pb-3 pt-1">
|
||||
{blacklist.map((item: any) => (
|
||||
<UserMuted key={item.id} data={item} />
|
||||
<MutedItem key={item.id} data={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -49,4 +49,4 @@ export const ChannelBlackList = ({ blacklist }: { blacklist: any }) => {
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
}
|
@ -1,20 +1,19 @@
|
||||
import { AccountContext } from '@lume/shared/accountProvider';
|
||||
import { AvatarUploader } from '@lume/shared/avatarUploader';
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { dateToUnix } from '@lume/utils/getDate';
|
||||
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
||||
import { createChannel } from '@lume/utils/storage';
|
||||
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { Cancel, Plus } from 'iconoir-react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { Fragment, useContext, useEffect, useState } from 'react';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
export const CreateChannelModal = () => {
|
||||
const pool: any = useContext(RelayContext);
|
||||
const activeAccount: any = useContext(AccountContext);
|
||||
export default function ChannelCreateModal() {
|
||||
const { account, isError, isLoading } = useActiveAccount();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [image, setImage] = useState(DEFAULT_AVATAR);
|
||||
@ -39,28 +38,33 @@ export const CreateChannelModal = () => {
|
||||
const onSubmit = (data: any) => {
|
||||
setLoading(true);
|
||||
|
||||
const event: any = {
|
||||
content: JSON.stringify(data),
|
||||
created_at: dateToUnix(),
|
||||
kind: 40,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, activeAccount.privkey);
|
||||
if (!isError && !isLoading && account) {
|
||||
const pool = new RelayPool(WRITEONLY_RELAYS);
|
||||
const event: any = {
|
||||
content: JSON.stringify(data),
|
||||
created_at: dateToUnix(),
|
||||
kind: 40,
|
||||
pubkey: account.pubkey,
|
||||
tags: [],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, account.privkey);
|
||||
|
||||
// publish channel
|
||||
pool.publish(event, WRITEONLY_RELAYS);
|
||||
// insert to database
|
||||
createChannel(event.id, event.pubkey, event.content, event.created_at);
|
||||
// reset form
|
||||
reset();
|
||||
setTimeout(() => {
|
||||
// close modal
|
||||
setIsOpen(false);
|
||||
// redirect to channel page
|
||||
navigate(`/channel?id=${event.id}`);
|
||||
}, 2000);
|
||||
// publish channel
|
||||
pool.publish(event, WRITEONLY_RELAYS);
|
||||
// insert to database
|
||||
createChannel(event.id, event.pubkey, event.content, event.created_at);
|
||||
// reset form
|
||||
reset();
|
||||
setTimeout(() => {
|
||||
// close modal
|
||||
setIsOpen(false);
|
||||
// redirect to channel page
|
||||
navigate(`/channel?id=${event.id}`);
|
||||
}, 2000);
|
||||
} else {
|
||||
console.log('error');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -237,4 +241,4 @@ export const CreateChannelModal = () => {
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
@ -4,7 +4,7 @@ import { usePageContext } from '@lume/utils/hooks/usePageContext';
|
||||
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const ChannelListItem = ({ data }: { data: any }) => {
|
||||
export default function ChannelsListItem({ data }: { data: any }) {
|
||||
const channel: any = useChannelMetadata(data.event_id, data.pubkey);
|
||||
const pageContext = usePageContext();
|
||||
|
||||
@ -13,7 +13,7 @@ export const ChannelListItem = ({ data }: { data: any }) => {
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/channel?id=${data.event_id}&pubkey=${data.pubkey}`}
|
||||
href={`/app/channel?id=${data.event_id}&pubkey=${data.pubkey}`}
|
||||
className={twMerge(
|
||||
'inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900',
|
||||
pageID === data.event_id ? 'dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800' : ''
|
||||
@ -31,4 +31,4 @@ export const ChannelListItem = ({ data }: { data: any }) => {
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
}
|
34
src/app/channel/components/list.tsx
Normal file
34
src/app/channel/components/list.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import ChannelCreateModal from '@lume/app/channel/components/createModal';
|
||||
import ChannelsListItem from '@lume/app/channel/components/item';
|
||||
import { getChannels } from '@lume/utils/storage';
|
||||
|
||||
import useSWR from 'swr';
|
||||
|
||||
const fetcher = () => getChannels(10, 0);
|
||||
|
||||
export default function ChannelsList() {
|
||||
const { data, error }: any = useSWR('channels', fetcher);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-px">
|
||||
<>
|
||||
{error && <div>failed to fetch</div>}
|
||||
{!data ? (
|
||||
<>
|
||||
<div className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5">
|
||||
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div>
|
||||
<div className="h-3 w-full animate-pulse bg-zinc-800"></div>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5">
|
||||
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div>
|
||||
<div className="h-3 w-full animate-pulse bg-zinc-800"></div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
data.map((item: { event_id: string }) => <ChannelsListItem key={item.event_id} data={item} />)
|
||||
)}
|
||||
</>
|
||||
<ChannelCreateModal />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
import { ChannelMessageItem } from '@lume/shared/channels/messages/item';
|
||||
import { Placeholder } from '@lume/shared/note/placeholder';
|
||||
import ChannelMessageItem from '@lume/app/channel/components/messages/item';
|
||||
import { sortedChannelMessagesAtom } from '@lume/stores/channel';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
export default function ChannelMessages() {
|
||||
export default function ChannelMessageList() {
|
||||
const virtuosoRef = useRef(null);
|
||||
const data = useAtomValue(sortedChannelMessagesAtom);
|
||||
|
||||
@ -29,7 +28,6 @@ export default function ChannelMessages() {
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={data}
|
||||
components={COMPONENTS}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
initialTopMostItemIndex={data.length - 1}
|
||||
@ -42,7 +40,3 @@ export default function ChannelMessages() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const COMPONENTS = {
|
||||
EmptyPlaceholder: () => <Placeholder />,
|
||||
};
|
123
src/app/channel/components/messages/form.tsx
Normal file
123
src/app/channel/components/messages/form.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import UserReply from '@lume/app/channel/components/messages/userReply';
|
||||
import { ImagePicker } from '@lume/shared/form/imagePicker';
|
||||
import { channelContentAtom, channelReplyAtom } from '@lume/stores/channel';
|
||||
import { FULL_RELAYS, WRITEONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { dateToUnix } from '@lume/utils/getDate';
|
||||
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
||||
|
||||
import { Cancel } from 'iconoir-react';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useResetAtom } from 'jotai/utils';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
|
||||
export default function ChannelMessageForm({ channelID }: { channelID: string | string[] }) {
|
||||
const { account, isLoading, isError } = useActiveAccount();
|
||||
|
||||
const [value, setValue] = useAtom(channelContentAtom);
|
||||
const resetValue = useResetAtom(channelContentAtom);
|
||||
|
||||
const channelReply = useAtomValue(channelReplyAtom);
|
||||
const resetChannelReply = useResetAtom(channelReplyAtom);
|
||||
|
||||
const submitEvent = () => {
|
||||
let tags: any[][];
|
||||
|
||||
if (channelReply.id !== null) {
|
||||
tags = [
|
||||
['e', channelID, '', 'root'],
|
||||
['e', channelReply.id, '', 'reply'],
|
||||
['p', channelReply.pubkey, ''],
|
||||
];
|
||||
} else {
|
||||
tags = [['e', channelID, '', 'root']];
|
||||
}
|
||||
|
||||
if (!isError && !isLoading && account) {
|
||||
const pool = new RelayPool(WRITEONLY_RELAYS);
|
||||
const event: any = {
|
||||
content: value,
|
||||
created_at: dateToUnix(),
|
||||
kind: 42,
|
||||
pubkey: account.pubkey,
|
||||
tags: tags,
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, account.privkey);
|
||||
|
||||
// publish note
|
||||
pool.publish(event, FULL_RELAYS);
|
||||
// reset state
|
||||
resetValue();
|
||||
// reset channel reply
|
||||
resetChannelReply();
|
||||
} else {
|
||||
console.log('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
submitEvent();
|
||||
}
|
||||
};
|
||||
|
||||
const stopReply = () => {
|
||||
resetChannelReply();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative ${
|
||||
channelReply.id ? 'h-36' : 'h-24'
|
||||
} w-full overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-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-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20`}
|
||||
>
|
||||
{channelReply.id && (
|
||||
<div className="absolute left-0 top-0 z-10 h-14 w-full p-[2px]">
|
||||
<div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3">
|
||||
<div className="flex w-full flex-col">
|
||||
<UserReply pubkey={channelReply.pubkey} />
|
||||
<div className="-mt-3.5 pl-[32px]">
|
||||
<div className="text-xs text-zinc-200">{channelReply.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => stopReply()}
|
||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
|
||||
>
|
||||
<Cancel width={12} height={12} className="text-zinc-100" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleEnterPress}
|
||||
spellCheck={false}
|
||||
placeholder="Message"
|
||||
className={`relative ${
|
||||
channelReply.id ? 'h-36 pt-16' : 'h-24 pt-3'
|
||||
} w-full resize-none rounded-lg border border-black/5 px-3.5 pb-3 text-sm 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 className="absolute bottom-2 w-full px-2">
|
||||
<div className="flex w-full items-center justify-between bg-zinc-800">
|
||||
<div className="flex items-center gap-2 divide-x divide-zinc-700">
|
||||
<ImagePicker type="channel" />
|
||||
<div className="flex items-center gap-2 pl-2"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => submitEvent()}
|
||||
disabled={value.length === 0 ? true : false}
|
||||
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
43
src/app/channel/components/messages/hideButton.tsx
Normal file
43
src/app/channel/components/messages/hideButton.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import Tooltip from '@lume/shared/tooltip';
|
||||
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { dateToUnix } from '@lume/utils/getDate';
|
||||
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
||||
|
||||
import { EyeClose } from 'iconoir-react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
|
||||
export default function MessageHideButton({ id }: { id: string }) {
|
||||
const { account, isError, isLoading } = useActiveAccount();
|
||||
|
||||
const hideMessage = () => {
|
||||
if (!isError && !isLoading && account) {
|
||||
const pool = new RelayPool(WRITEONLY_RELAYS);
|
||||
const event: any = {
|
||||
content: '',
|
||||
created_at: dateToUnix(),
|
||||
kind: 43,
|
||||
pubkey: account.pubkey,
|
||||
tags: [['e', id]],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, account.privkey);
|
||||
|
||||
// publish note
|
||||
pool.publish(event, WRITEONLY_RELAYS);
|
||||
} else {
|
||||
console.log('error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip message="Hide this message">
|
||||
<button
|
||||
onClick={() => hideMessage()}
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
|
||||
>
|
||||
<EyeClose width={16} height={16} className="text-zinc-400" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
31
src/app/channel/components/messages/item.tsx
Normal file
31
src/app/channel/components/messages/item.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import MessageHideButton from '@lume/app/channel/components/messages/hideButton';
|
||||
import MessageMuteButton from '@lume/app/channel/components/messages/muteButton';
|
||||
import MessageReplyButton from '@lume/app/channel/components/messages/replyButton';
|
||||
import ChannelMessageUser from '@lume/app/channel/components/messages/user';
|
||||
import { messageParser } from '@lume/utils/parser';
|
||||
|
||||
export default function ChannelMessageItem({ data }: { data: any }) {
|
||||
const content = messageParser(data.content);
|
||||
|
||||
return (
|
||||
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-2 hover:bg-black/20">
|
||||
<div className="flex flex-col">
|
||||
<ChannelMessageUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-[17px] pl-[48px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="whitespace-pre-line break-words break-words text-sm leading-tight">
|
||||
{data.hide ? <span>[hided message]</span> : content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
|
||||
<div className="inline-flex h-7 items-center justify-center gap-1 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
|
||||
<MessageReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
|
||||
<MessageHideButton id={data.id} />
|
||||
<MessageMuteButton pubkey={data.pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
43
src/app/channel/components/messages/muteButton.tsx
Normal file
43
src/app/channel/components/messages/muteButton.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import Tooltip from '@lume/shared/tooltip';
|
||||
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { dateToUnix } from '@lume/utils/getDate';
|
||||
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
||||
|
||||
import { MicMute } from 'iconoir-react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
|
||||
export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
||||
const { account, isError, isLoading } = useActiveAccount();
|
||||
|
||||
const muteUser = () => {
|
||||
if (!isError && !isLoading && account) {
|
||||
const pool = new RelayPool(WRITEONLY_RELAYS);
|
||||
const event: any = {
|
||||
content: '',
|
||||
created_at: dateToUnix(),
|
||||
kind: 44,
|
||||
pubkey: account.pubkey,
|
||||
tags: [['p', pubkey]],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, account.privkey);
|
||||
|
||||
// publish note
|
||||
pool.publish(event, WRITEONLY_RELAYS);
|
||||
} else {
|
||||
console.log('error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip message="Mute this user">
|
||||
<button
|
||||
onClick={() => muteUser()}
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
|
||||
>
|
||||
<MicMute width={16} height={16} className="text-zinc-400" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
@ -4,7 +4,7 @@ import { channelReplyAtom } from '@lume/stores/channel';
|
||||
import { Reply } from 'iconoir-react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
|
||||
export const ReplyButton = ({ id, pubkey, content }: { id: string; pubkey: string; content: string }) => {
|
||||
export default function MessageReplyButton({ id, pubkey, content }: { id: string; pubkey: string; content: string }) {
|
||||
const setChannelReplyAtom = useSetAtom(channelReplyAtom);
|
||||
|
||||
const createReply = () => {
|
||||
@ -21,4 +21,4 @@ export const ReplyButton = ({ id, pubkey, content }: { id: string; pubkey: strin
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
}
|
48
src/app/channel/components/messages/user.tsx
Normal file
48
src/app/channel/components/messages/user.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export default function ChannelMessageUser({ pubkey, time }: { pubkey: string; time: number }) {
|
||||
const { user, isError, isLoading } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex items-start gap-3">
|
||||
{isError || isLoading ? (
|
||||
<>
|
||||
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-9 w-9 shrink rounded-md">
|
||||
<img
|
||||
src={user?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-9 w-9 rounded-md object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
<span className="font-semibold leading-none text-zinc-200 group-hover:underline">
|
||||
{user?.display_name || user?.name || shortenKey(pubkey)}
|
||||
</span>
|
||||
<span className="leading-none text-zinc-500">·</span>
|
||||
<span className="leading-none text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
33
src/app/channel/components/messages/userReply.tsx
Normal file
33
src/app/channel/components/messages/userReply.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
export default function UserReply({ pubkey }: { pubkey: string }) {
|
||||
const { user, isError, isLoading } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex items-start gap-1">
|
||||
{isError || isLoading ? (
|
||||
<>
|
||||
<div className="relative h-7 w-7 shrink animate-pulse overflow-hidden rounded bg-zinc-800"></div>
|
||||
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-xs font-medium leading-none text-zinc-500"></span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-7 w-7 shrink overflow-hidden rounded">
|
||||
<img
|
||||
src={user?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-7 w-7 rounded object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-medium leading-none text-zinc-500">
|
||||
Replying to {user?.name || shortenKey(pubkey)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -4,7 +4,7 @@ import { useChannelProfile } from '@lume/utils/hooks/useChannelProfile';
|
||||
import { Copy } from 'iconoir-react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
export const ChannelProfile = ({ id, pubkey }: { id: string; pubkey: string }) => {
|
||||
export default function ChannelMetadata({ id, pubkey }: { id: string; pubkey: string }) {
|
||||
const metadata = useChannelProfile(id, pubkey);
|
||||
const noteID = id ? nip19.noteEncode(id) : null;
|
||||
|
||||
@ -37,4 +37,4 @@ export const ChannelProfile = ({ id, pubkey }: { id: string; pubkey: string }) =
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
78
src/app/channel/components/mutedItem.tsx
Normal file
78
src/app/channel/components/mutedItem.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function MutedItem({ data }: { data: any }) {
|
||||
const { user, isError, isLoading } = useProfile(data.content);
|
||||
const [status, setStatus] = useState(data.status);
|
||||
|
||||
const unmute = async () => {
|
||||
const { updateItemInBlacklist } = await import('@lume/utils/storage');
|
||||
const res = await updateItemInBlacklist(data.content, 0);
|
||||
if (res) {
|
||||
setStatus(0);
|
||||
}
|
||||
};
|
||||
|
||||
const mute = async () => {
|
||||
const { updateItemInBlacklist } = await import('@lume/utils/storage');
|
||||
const res = await updateItemInBlacklist(data.content, 1);
|
||||
if (res) {
|
||||
setStatus(1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
{isError || isLoading ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div>
|
||||
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
|
||||
<div className="h-3 w-16 animate-pulse bg-zinc-800"></div>
|
||||
<div className="h-2 w-10 animate-pulse bg-zinc-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="relative h-9 w-9 shrink rounded-md">
|
||||
<img
|
||||
src={user?.picture || DEFAULT_AVATAR}
|
||||
alt={data.content}
|
||||
className="h-9 w-9 rounded-md object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
|
||||
<span className="truncate text-sm font-medium leading-none text-zinc-200">
|
||||
{user?.display_name || user?.name}
|
||||
</span>
|
||||
<span className="text-xs leading-none text-zinc-400">{shortenKey(data.content)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{status === 1 ? (
|
||||
<button
|
||||
onClick={() => unmute()}
|
||||
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-xs font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
|
||||
>
|
||||
Unmute
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => mute()}
|
||||
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-xs font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
|
||||
>
|
||||
Mute
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,19 +1,18 @@
|
||||
import { AccountContext } from '@lume/shared/accountProvider';
|
||||
import { AvatarUploader } from '@lume/shared/avatarUploader';
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { dateToUnix } from '@lume/utils/getDate';
|
||||
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
||||
import { getChannel, updateChannelMetadata } from '@lume/utils/storage';
|
||||
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { Cancel, EditPencil } from 'iconoir-react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { Fragment, useContext, useEffect, useState } from 'react';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export const UpdateChannelModal = ({ id }: { id: string }) => {
|
||||
const pool: any = useContext(RelayContext);
|
||||
const activeAccount: any = useContext(AccountContext);
|
||||
export default function ChannelUpdateModal({ id }: { id: string }) {
|
||||
const { account, isError, isLoading } = useActiveAccount();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [image, setImage] = useState(DEFAULT_AVATAR);
|
||||
@ -36,35 +35,44 @@ export const UpdateChannelModal = ({ id }: { id: string }) => {
|
||||
} = useForm({
|
||||
defaultValues: async () => {
|
||||
const channel = await getChannel(id);
|
||||
const metadata = JSON.parse(channel.metadata);
|
||||
// update image state
|
||||
setImage(metadata.picture);
|
||||
// set default values
|
||||
return metadata;
|
||||
if (channel) {
|
||||
const metadata = JSON.parse(channel.metadata);
|
||||
// update image state
|
||||
setImage(metadata.picture);
|
||||
// set default values
|
||||
return metadata;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
setLoading(true);
|
||||
|
||||
const event: any = {
|
||||
content: JSON.stringify(data),
|
||||
created_at: dateToUnix(),
|
||||
kind: 41,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, activeAccount.privkey);
|
||||
if (!isError && !isLoading && account) {
|
||||
const pool = new RelayPool(WRITEONLY_RELAYS);
|
||||
const event: any = {
|
||||
content: JSON.stringify(data),
|
||||
created_at: dateToUnix(),
|
||||
kind: 41,
|
||||
pubkey: account.pubkey,
|
||||
tags: [],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, account.privkey);
|
||||
|
||||
// publish channel
|
||||
pool.publish(event, WRITEONLY_RELAYS);
|
||||
// update channel metadata in database
|
||||
updateChannelMetadata(event.id, event.content);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
setIsOpen(false);
|
||||
// publish channel
|
||||
pool.publish(event, WRITEONLY_RELAYS);
|
||||
// update channel metadata in database
|
||||
updateChannelMetadata(event.id, event.content);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
console.log('error');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -236,4 +244,4 @@ export const UpdateChannelModal = ({ id }: { id: string }) => {
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
@ -1,34 +1,34 @@
|
||||
import { ChannelBlackList } from '@lume/shared/channels/channelBlackList';
|
||||
import { ChannelProfile } from '@lume/shared/channels/channelProfile';
|
||||
import { UpdateChannelModal } from '@lume/shared/channels/updateChannelModal';
|
||||
import { FormChannel } from '@lume/shared/form/channel';
|
||||
import ChannelBlackList from '@lume/app/channel/components/blacklist';
|
||||
import ChannelMessageForm from '@lume/app/channel/components/messages/form';
|
||||
import ChannelMetadata from '@lume/app/channel/components/metadata';
|
||||
import ChannelUpdateModal from '@lume/app/channel/components/updateModal';
|
||||
import { channelMessagesAtom, channelReplyAtom } from '@lume/stores/channel';
|
||||
import { FULL_RELAYS } from '@lume/stores/constants';
|
||||
import { dateToUnix, hoursAgo } from '@lume/utils/getDate';
|
||||
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
||||
import { usePageContext } from '@lume/utils/hooks/usePageContext';
|
||||
import { arrayObjToPureArr } from '@lume/utils/transform';
|
||||
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useResetAtom } from 'jotai/utils';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { Suspense, lazy, useRef } from 'react';
|
||||
import { Suspense, lazy, useEffect, useRef } from 'react';
|
||||
import useSWRSubscription from 'swr/subscription';
|
||||
|
||||
const ChannelMessages = lazy(() => import('@lume/shared/channels/messages'));
|
||||
|
||||
let mutedList: any = [];
|
||||
let activeAccount: any = {};
|
||||
let activeMutedList: any = [];
|
||||
let activeHidedList: any = [];
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const { getBlacklist, getActiveBlacklist, getActiveAccount } = await import('@lume/utils/storage');
|
||||
activeAccount = await getActiveAccount();
|
||||
const activeAccount = await getActiveAccount();
|
||||
activeHidedList = await getActiveBlacklist(activeAccount.id, 43);
|
||||
activeMutedList = await getActiveBlacklist(activeAccount.id, 44);
|
||||
mutedList = await getBlacklist(activeAccount.id, 44);
|
||||
}
|
||||
|
||||
const ChannelMessageList = lazy(() => import('@lume/app/channel/components/messageList'));
|
||||
|
||||
export function Page() {
|
||||
const pageContext = usePageContext();
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
@ -36,6 +36,8 @@ export function Page() {
|
||||
const channelID = searchParams.id;
|
||||
const channelPubkey = searchParams.pubkey;
|
||||
|
||||
const { account, isLoading, isError } = useActiveAccount();
|
||||
|
||||
const setChannelMessages = useSetAtom(channelMessagesAtom);
|
||||
const resetChannelMessages = useResetAtom(channelMessagesAtom);
|
||||
const resetChannelReply = useResetAtom(channelReplyAtom);
|
||||
@ -44,29 +46,26 @@ export function Page() {
|
||||
const hided = arrayObjToPureArr(activeHidedList);
|
||||
const muted = arrayObjToPureArr(activeMutedList);
|
||||
|
||||
useSWRSubscription(channelID, () => {
|
||||
// reset channel reply
|
||||
resetChannelReply();
|
||||
// reset channel messages
|
||||
resetChannelMessages();
|
||||
// subscribe for new messages
|
||||
useSWRSubscription(channelID ? channelID : null, (key: string, {}: any) => {
|
||||
// subscribe to channel
|
||||
const pool = new RelayPool(FULL_RELAYS);
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
'#e': [channelID],
|
||||
'#e': [key],
|
||||
kinds: [42],
|
||||
since: dateToUnix(hoursAgo(48, now.current)),
|
||||
since: dateToUnix(hoursAgo(72, now.current)),
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
FULL_RELAYS,
|
||||
(event: { kind: number; tags: string[][]; pubkey: string; id: string }) => {
|
||||
if (muted.includes(event.pubkey)) {
|
||||
console.log('muted');
|
||||
} else if (hided.includes(event.id)) {
|
||||
console.log('hided');
|
||||
} else {
|
||||
setChannelMessages((prev) => [...prev, event]);
|
||||
(event) => {
|
||||
const message: any = event;
|
||||
if (hided.includes(event.id)) {
|
||||
message.push({ hide: true });
|
||||
}
|
||||
if (!muted.includes(event.pubkey)) {
|
||||
setChannelMessages((prev) => [...prev, message]);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -76,23 +75,34 @@ export function Page() {
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// reset channel reply
|
||||
resetChannelReply();
|
||||
// reset channel messages
|
||||
resetChannelMessages();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-between gap-2">
|
||||
<div className="flex h-11 w-full shrink-0 items-center justify-between">
|
||||
<div>
|
||||
<ChannelProfile id={channelID} pubkey={channelPubkey} />
|
||||
<ChannelMetadata id={channelID} pubkey={channelPubkey} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChannelBlackList blacklist={mutedList} />
|
||||
{activeAccount.pubkey === channelPubkey && <UpdateChannelModal id={activeAccount} />}
|
||||
{!isLoading && !isError && account ? (
|
||||
account.pubkey === channelPubkey && <ChannelUpdateModal id={account.id} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex w-full flex-1 flex-col justify-between rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<ChannelMessages />
|
||||
<ChannelMessageList />
|
||||
</Suspense>
|
||||
<div className="shrink-0 p-3">
|
||||
<FormChannel eventId={channelID} />
|
||||
<div className="inline-flex shrink-0 p-3">
|
||||
<ChannelMessageForm channelID={channelID} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ImagePreview } from '@lume/app/newsfeed/components/note/preview/image';
|
||||
import { VideoPreview } from '@lume/app/newsfeed/components/note/preview/video';
|
||||
import { YoutubePreview } from '@lume/app/newsfeed/components/note/preview/youtube';
|
||||
import { NoteQuote } from '@lume/app/newsfeed/components/note/quote';
|
||||
import { NoteMentionUser } from '@lume/app/newsfeed/components/user/mention';
|
||||
import ImagePreview from '@lume/shared/preview/image';
|
||||
import VideoPreview from '@lume/shared/preview/video';
|
||||
import YoutubePreview from '@lume/shared/preview/youtube';
|
||||
|
||||
import destr from 'destr';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
@ -8,32 +8,47 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const NoteDefaultUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
const profile = useProfile(pubkey);
|
||||
const { user, isError, isLoading } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex h-11 items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h5 className="text-sm font-semibold leading-none group-hover:underline">
|
||||
{profile?.display_name || profile?.name || shortenKey(pubkey)}
|
||||
</h5>
|
||||
<span className="text-sm leading-none text-zinc-700"></span>
|
||||
{isError || isLoading ? (
|
||||
<>
|
||||
<div className="relative h-11 w-11 shrink animate-pulse overflow-hidden rounded-md bg-white bg-zinc-800"></div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div>
|
||||
</div>
|
||||
<div className="h-2.5 w-14 animate-pulse rounded bg-zinc-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm leading-none text-zinc-500">
|
||||
{profile?.nip05 || shortenKey(pubkey)} • {dayjs().to(dayjs.unix(time))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<img
|
||||
src={user?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h5 className="text-sm font-semibold leading-none group-hover:underline">
|
||||
{user?.display_name || user?.name || shortenKey(pubkey)}
|
||||
</h5>
|
||||
</div>
|
||||
<span className="text-sm leading-none text-zinc-500">
|
||||
{user?.nip05 || shortenKey(pubkey)} • {dayjs().to(dayjs.unix(time))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,8 +2,15 @@ import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
export const NoteMentionUser = ({ pubkey }: { pubkey: string }) => {
|
||||
const profile = useProfile(pubkey);
|
||||
const { user, isError, isLoading } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<span className="cursor-pointer text-fuchsia-500">@{profile?.name || profile?.username || shortenKey(pubkey)}</span>
|
||||
<>
|
||||
{isError || isLoading ? (
|
||||
<span className="inline-flex h-4 w-10 animate-pulse rounded bg-zinc-800"></span>
|
||||
) : (
|
||||
<span className="cursor-pointer text-fuchsia-500">@{user?.username || user?.name || shortenKey(pubkey)}</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -8,29 +8,44 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const NoteRepostUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
const profile = useProfile(pubkey);
|
||||
const { user, isError, isLoading } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
<h5 className="font-semibold leading-tight group-hover:underline">
|
||||
{profile?.display_name || profile?.name || shortenKey(pubkey)}{' '}
|
||||
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
|
||||
reposted
|
||||
</span>
|
||||
</h5>
|
||||
<span className="leading-tight text-zinc-500">·</span>
|
||||
<span className="text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
|
||||
</div>
|
||||
{isError || isLoading ? (
|
||||
<>
|
||||
<div className="relative h-11 w-11 shrink animate-pulse overflow-hidden rounded-md bg-white bg-zinc-800"></div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<img
|
||||
src={user?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
<h5 className="font-semibold leading-tight group-hover:underline">
|
||||
{user?.display_name || user?.name || shortenKey(pubkey)}{' '}
|
||||
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
|
||||
reposted
|
||||
</span>
|
||||
</h5>
|
||||
<span className="leading-tight text-zinc-500">·</span>
|
||||
<span className="text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
|
||||
export const ActiveAccount = ({ user }: { user: any }) => {
|
||||
export default function ActiveAccount({ user }: { user: any }) {
|
||||
const userData = JSON.parse(user.metadata);
|
||||
|
||||
return (
|
||||
@ -8,4 +8,4 @@ export const ActiveAccount = ({ user }: { user: any }) => {
|
||||
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
}
|
11
src/shared/accounts/inactive.tsx
Normal file
11
src/shared/accounts/inactive.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
|
||||
export default function InactiveAccount({ user }: { user: any }) {
|
||||
const userData = JSON.parse(user.metadata);
|
||||
|
||||
return (
|
||||
<div className="relative h-11 w-11 shrink rounded-lg">
|
||||
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { ChannelListItem } from '@lume/shared/channels/channelListItem';
|
||||
import { CreateChannelModal } from '@lume/shared/channels/createChannelModal';
|
||||
|
||||
let channels: any = [];
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const { getChannels } = await import('@lume/utils/storage');
|
||||
channels = await getChannels(100, 0);
|
||||
}
|
||||
|
||||
export default function ChannelList() {
|
||||
return (
|
||||
<div className="flex flex-col gap-px">
|
||||
{channels.map((item: { event_id: string }) => (
|
||||
<ChannelListItem key={item.event_id} data={item} />
|
||||
))}
|
||||
<CreateChannelModal />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { AccountContext } from '@lume/shared/accountProvider';
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import Tooltip from '@lume/shared/tooltip';
|
||||
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { dateToUnix } from '@lume/utils/getDate';
|
||||
|
||||
import { EyeClose } from 'iconoir-react';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
export const HideMessageButton = ({ id }: { id: string }) => {
|
||||
const pool: any = useContext(RelayContext);
|
||||
const activeAccount: any = useContext(AccountContext);
|
||||
|
||||
const hideMessage = useCallback(() => {
|
||||
const event: any = {
|
||||
content: '',
|
||||
created_at: dateToUnix(),
|
||||
kind: 43,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [['e', id]],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, activeAccount.privkey);
|
||||
|
||||
// publish note
|
||||
pool.publish(event, WRITEONLY_RELAYS);
|
||||
}, [activeAccount.pubkey, activeAccount.privkey, id, pool]);
|
||||
|
||||
return (
|
||||
<Tooltip message="Hide this message">
|
||||
<button
|
||||
onClick={() => hideMessage()}
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
|
||||
>
|
||||
<EyeClose width={16} height={16} className="text-zinc-400" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import { HideMessageButton } from '@lume/shared/channels/messages/hideMessageButton';
|
||||
import { MuteButton } from '@lume/shared/channels/messages/muteButton';
|
||||
import { ReplyButton } from '@lume/shared/channels/messages/replyButton';
|
||||
import { MessageUser } from '@lume/shared/chats/messageUser';
|
||||
import { messageParser } from '@lume/utils/parser';
|
||||
|
||||
import { memo } from 'react';
|
||||
|
||||
export const ChannelMessageItem = memo(function ChannelMessageItem({ data }: { data: any }) {
|
||||
const content = messageParser(data.content);
|
||||
|
||||
return (
|
||||
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-2 hover:bg-black/20">
|
||||
<div className="flex flex-col">
|
||||
<MessageUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-[17px] pl-[48px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="prose prose-zinc max-w-none break-words text-sm leading-tight dark:prose-invert prose-p:m-0 prose-p:text-sm prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
|
||||
<div className="inline-flex h-7 items-center justify-center gap-1 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
|
||||
<ReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
|
||||
<HideMessageButton id={data.id} />
|
||||
<MuteButton pubkey={data.pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,40 +0,0 @@
|
||||
import { AccountContext } from '@lume/shared/accountProvider';
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import Tooltip from '@lume/shared/tooltip';
|
||||
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { dateToUnix } from '@lume/utils/getDate';
|
||||
|
||||
import { MicMute } from 'iconoir-react';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const MuteButton = ({ pubkey }: { pubkey: string }) => {
|
||||
const pool: any = useContext(RelayContext);
|
||||
const activeAccount: any = useContext(AccountContext);
|
||||
|
||||
const muteUser = () => {
|
||||
const event: any = {
|
||||
content: '',
|
||||
created_at: dateToUnix(),
|
||||
kind: 44,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [['p', pubkey]],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, activeAccount.privkey);
|
||||
|
||||
// publish note
|
||||
pool.publish(event, WRITEONLY_RELAYS);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip message="Mute this user">
|
||||
<button
|
||||
onClick={() => muteUser()}
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
|
||||
>
|
||||
<MicMute width={16} height={16} className="text-zinc-400" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
@ -1,38 +1,40 @@
|
||||
import { AccountContext } from '@lume/shared/accountProvider';
|
||||
import ActiveAccount from '@lume/shared/accounts/active';
|
||||
import InactiveAccount from '@lume/shared/accounts/inactive';
|
||||
import LumeIcon from '@lume/shared/icons/lume';
|
||||
import { ActiveAccount } from '@lume/shared/multiAccounts/activeAccount';
|
||||
import { InactiveAccount } from '@lume/shared/multiAccounts/inactiveAccount';
|
||||
import { APP_VERSION } from '@lume/stores/constants';
|
||||
import { getAccounts } from '@lume/utils/storage';
|
||||
|
||||
import { Plus } from 'iconoir-react';
|
||||
import { useContext } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
let accounts: any = [];
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const { getAccounts } = await import('@lume/utils/storage');
|
||||
accounts = await getAccounts();
|
||||
}
|
||||
const fetcher = () => getAccounts();
|
||||
|
||||
export default function MultiAccounts() {
|
||||
const activeAccount: any = useContext(AccountContext);
|
||||
const { data, error }: any = useSWR('allAccounts', fetcher);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-between px-2 pb-4 pt-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<a
|
||||
href="/explore"
|
||||
href="/app/newsfeed/following"
|
||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg bg-zinc-900 hover:bg-zinc-800"
|
||||
>
|
||||
<LumeIcon className="h-6 w-auto text-zinc-400 group-hover:text-zinc-200" />
|
||||
</a>
|
||||
{accounts.map((account: { pubkey: string }) => {
|
||||
if (account.pubkey === activeAccount.pubkey) {
|
||||
return <ActiveAccount key={account.pubkey} user={account} />;
|
||||
} else {
|
||||
return <InactiveAccount key={account.pubkey} user={account} />;
|
||||
}
|
||||
})}
|
||||
<>
|
||||
{error && <div>failed to load</div>}
|
||||
{!data ? (
|
||||
<div className="group relative flex h-11 w-11 shrink animate-pulse cursor-pointer items-center justify-center rounded-lg bg-zinc-900"></div>
|
||||
) : (
|
||||
data.map((account: { is_active: number; pubkey: string }) => {
|
||||
if (account.is_active === 1) {
|
||||
return <ActiveAccount key={account.pubkey} user={account} />;
|
||||
} else {
|
||||
return <InactiveAccount key={account.pubkey} user={account} />;
|
||||
}
|
||||
})
|
||||
)}
|
||||
</>
|
||||
<a
|
||||
href="/onboarding"
|
||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-zinc-600 hover:border-zinc-400"
|
@ -1,17 +0,0 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
|
||||
import { memo } from 'react';
|
||||
|
||||
export const InactiveAccount = memo(function InactiveAccount({ user }: { user: any }) {
|
||||
const userData = JSON.parse(user.metadata);
|
||||
|
||||
const setCurrentUser = () => {
|
||||
console.log('clicked');
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-lg">
|
||||
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
|
||||
</button>
|
||||
);
|
||||
});
|
@ -1,9 +1,8 @@
|
||||
import ChannelsList from '@lume/app/channel/components/list';
|
||||
import ActiveLink from '@lume/shared/activeLink';
|
||||
import ChannelList from '@lume/shared/channels/channelList';
|
||||
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import { Bonfire, NavArrowUp, PeopleTag } from 'iconoir-react';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export default function Navigation() {
|
||||
return (
|
||||
@ -58,9 +57,7 @@ export default function Navigation() {
|
||||
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Channels</h3>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel>
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<ChannelList />
|
||||
</Suspense>
|
||||
<ChannelsList />
|
||||
</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const ImagePreview = ({ url, size }: { url: string; size: string }) => {
|
||||
export default function ImagePreview({ url, size }: { url: string; size: string }) {
|
||||
return (
|
||||
<div className={`relative h-full ${size === 'large' ? 'w-4/5' : 'w-1/2'} mt-2 rounded-lg border border-zinc-800`}>
|
||||
<img
|
||||
@ -11,4 +11,4 @@ export const ImagePreview = ({ url, size }: { url: string; size: string }) => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { MediaOutlet, MediaPlayer } from '@vidstack/react';
|
||||
|
||||
export const VideoPreview = ({ url }: { url: string }) => {
|
||||
export default function VideoPreview({ url }: { url: string }) {
|
||||
return (
|
||||
<div onClick={(e) => e.stopPropagation()} className="relative mt-2 flex flex-col overflow-hidden rounded-lg">
|
||||
<MediaPlayer src={url} poster="" controls>
|
||||
@ -8,4 +8,4 @@ export const VideoPreview = ({ url }: { url: string }) => {
|
||||
</MediaPlayer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
@ -5,7 +5,7 @@ function getVideoId(url: string) {
|
||||
return regex.exec(url)[3];
|
||||
}
|
||||
|
||||
export const YoutubePreview = ({ url }: { url: string }) => {
|
||||
export default function YoutubePreview({ url }: { url: string }) {
|
||||
const id = getVideoId(url);
|
||||
|
||||
return (
|
||||
@ -13,4 +13,4 @@ export const YoutubePreview = ({ url }: { url: string }) => {
|
||||
<YouTube videoId={id} className="aspect-video xl:w-2/3" opts={{ width: '100%', height: '100%' }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { createContext, useMemo } from 'react';
|
||||
|
||||
export const RelayContext = createContext({});
|
||||
|
||||
export default function RelayProvider({ children }: { children: React.ReactNode }) {
|
||||
const pool = useMemo(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return new RelayPool(READONLY_RELAYS, { useEventCache: false, logSubscriptions: false });
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
return <RelayContext.Provider value={pool}>{children}</RelayContext.Provider>;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
import { memo } from 'react';
|
||||
|
||||
export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
|
||||
const profile = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-full object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||
<span className="truncate font-medium leading-tight text-zinc-200">
|
||||
{profile?.display_name || profile?.name}
|
||||
</span>
|
||||
<span className="text-sm leading-tight text-zinc-400">{shortenKey(pubkey)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
export const UserFollow = ({ pubkey }: { pubkey: string }) => {
|
||||
const profile = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-full object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||
<span className="truncate font-medium leading-tight text-zinc-200">
|
||||
{profile?.display_name || profile?.name}
|
||||
</span>
|
||||
<span className="text-sm leading-tight text-zinc-400">{shortenKey(pubkey)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { MoreHoriz } from 'iconoir-react';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const UserLarge = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
const profile = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md border border-white/10 object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex-1">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="flex flex-col gap-1 text-sm">
|
||||
<span className="font-bold leading-tight text-zinc-100">
|
||||
{profile?.display_name || profile?.name || shortenKey(pubkey)}
|
||||
</span>
|
||||
<span className="leading-tight text-zinc-400">
|
||||
{profile?.username || shortenKey(pubkey)} · {dayjs().to(dayjs.unix(time))}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800">
|
||||
<MoreHoriz width={12} height={12} className="text-zinc-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
export const UserMini = ({ pubkey }: { pubkey: string }) => {
|
||||
const profile = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex items-start gap-1">
|
||||
<div className="relative h-7 w-7 shrink overflow-hidden rounded border border-white/10">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-7 w-7 rounded object-cover"
|
||||
loading="lazy"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-medium leading-none text-zinc-500">
|
||||
Replying to {profile?.name || shortenKey(pubkey)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,64 +0,0 @@
|
||||
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export const UserMuted = ({ data }: { data: any }) => {
|
||||
const profile = useProfile(data.content);
|
||||
const [status, setStatus] = useState(data.status);
|
||||
|
||||
const unmute = async () => {
|
||||
const { updateItemInBlacklist } = await import('@lume/utils/storage');
|
||||
const res = await updateItemInBlacklist(data.content, 0);
|
||||
if (res) {
|
||||
setStatus(0);
|
||||
}
|
||||
};
|
||||
|
||||
const mute = async () => {
|
||||
const { updateItemInBlacklist } = await import('@lume/utils/storage');
|
||||
const res = await updateItemInBlacklist(data.content, 1);
|
||||
if (res) {
|
||||
setStatus(1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="relative h-9 w-9 shrink rounded-md">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={data.content}
|
||||
className="h-9 w-9 rounded-md object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
|
||||
<span className="truncate text-sm font-medium leading-none text-zinc-200">
|
||||
{profile?.display_name || profile?.name}
|
||||
</span>
|
||||
<span className="text-xs leading-none text-zinc-400">{shortenKey(data.content)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{status === 1 ? (
|
||||
<button
|
||||
onClick={() => unmute()}
|
||||
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-xs font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
|
||||
>
|
||||
Unmute
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => mute()}
|
||||
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-xs font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
|
||||
>
|
||||
Mute
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,15 +1,15 @@
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { updateChannelMetadata } from '@lume/utils/storage';
|
||||
import { getChannel } from '@lume/utils/storage';
|
||||
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const useChannelMetadata = (id: string, channelPubkey: string) => {
|
||||
const pool: any = useContext(RelayContext);
|
||||
const [metadata, setMetadata] = useState(null);
|
||||
|
||||
const fetchFromRelay = useCallback(() => {
|
||||
const pool = new RelayPool(READONLY_RELAYS);
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
@ -53,7 +53,7 @@ export const useChannelMetadata = (id: string, channelPubkey: string) => {
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [channelPubkey, id, pool]);
|
||||
}, [channelPubkey, id]);
|
||||
|
||||
const getChannelFromDB = useCallback(async () => {
|
||||
return await getChannel(id);
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { FULL_RELAYS } from '@lume/stores/constants';
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import useSWRSubscription from 'swr/subscription';
|
||||
|
||||
export const useChannelProfile = (id: string, channelPubkey: string) => {
|
||||
const pool: any = useContext(RelayContext);
|
||||
|
||||
const { data } = useSWRSubscription(
|
||||
id
|
||||
? [
|
||||
@ -21,9 +18,10 @@ export const useChannelProfile = (id: string, channelPubkey: string) => {
|
||||
]
|
||||
: null,
|
||||
(key, { next }) => {
|
||||
const pool = new RelayPool(FULL_RELAYS);
|
||||
const unsubscribe = pool.subscribe(
|
||||
key,
|
||||
READONLY_RELAYS,
|
||||
FULL_RELAYS,
|
||||
(event: { kind: number; pubkey: string; content: string }) => {
|
||||
switch (event.kind) {
|
||||
case 40:
|
||||
@ -41,7 +39,6 @@ export const useChannelProfile = (id: string, channelPubkey: string) => {
|
||||
undefined,
|
||||
{
|
||||
unsubscribeOnEose: true,
|
||||
logAllEvents: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -3,12 +3,11 @@ import useSWR from 'swr';
|
||||
const fetcher = (url: string) => fetch(url).then((r: any) => r.json());
|
||||
|
||||
export const useProfile = (pubkey: string) => {
|
||||
const { data, error } = useSWR(`https://rbr.bio/${pubkey}/metadata.json`, fetcher);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
if (data) {
|
||||
return JSON.parse(data.content);
|
||||
}
|
||||
return null;
|
||||
const { data, error, isLoading } = useSWR(`https://us.rbr.bio/${pubkey}/metadata.json`, fetcher);
|
||||
|
||||
return {
|
||||
user: data ? JSON.parse(data.content ? data.content : null) : null,
|
||||
isLoading,
|
||||
isError: error,
|
||||
};
|
||||
};
|
||||
|
@ -1,60 +1,9 @@
|
||||
import { ImagePreview } from '@lume/shared/note/preview/image';
|
||||
import { VideoPreview } from '@lume/shared/note/preview/video';
|
||||
import { YoutubePreview } from '@lume/shared/note/preview/youtube';
|
||||
import { NoteQuote } from '@lume/shared/note/quote';
|
||||
import { UserMention } from '@lume/shared/user/mention';
|
||||
import ImagePreview from '@lume/shared/preview/image';
|
||||
import VideoPreview from '@lume/shared/preview/video';
|
||||
import YoutubePreview from '@lume/shared/preview/youtube';
|
||||
|
||||
import destr from 'destr';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
||||
export const contentParser = (noteContent: any, noteTags: any) => {
|
||||
let parsedContent = noteContent.trim();
|
||||
|
||||
// get data tags
|
||||
const tags = destr(noteTags);
|
||||
// handle urls
|
||||
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
|
||||
if (match.match(/\.(jpg|jpeg|gif|png|webp)$/i)) {
|
||||
// image url
|
||||
return <ImagePreview key={match + i} url={match} size="large" />;
|
||||
} else if (match.match(/(http:|https:)?(\/\/)?(www\.)?(youtube.com|youtu.be)\/(watch|embed)?(\?v=|\/)?(\S+)?/)) {
|
||||
// youtube
|
||||
return <YoutubePreview key={match + i} url={match} />;
|
||||
} else if (match.match(/\.(mp4|webm)$/i)) {
|
||||
// video
|
||||
return <VideoPreview key={match + i} url={match} />;
|
||||
} else {
|
||||
return (
|
||||
<a key={match + i} href={match} className="cursor-pointer text-fuchsia-500" target="_blank" rel="noreferrer">
|
||||
{match}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
});
|
||||
// handle #-hashtags
|
||||
parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => (
|
||||
<span key={match + i} className="cursor-pointer text-fuchsia-500">
|
||||
#{match}
|
||||
</span>
|
||||
));
|
||||
// handle mentions
|
||||
if (tags && tags.length > 0) {
|
||||
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
|
||||
if (tags[match][0] === 'p') {
|
||||
// @-mentions
|
||||
return <UserMention key={tags[match][1] + i} pubkey={tags[match][1]} />;
|
||||
} else if (tags[match][0] === 'e') {
|
||||
// note-quotes
|
||||
return <NoteQuote key={tags[match][1] + i} id={tags[match][1]} />;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return parsedContent;
|
||||
};
|
||||
|
||||
export const messageParser = (noteContent: any) => {
|
||||
let parsedContent = noteContent.trim();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user