update channel

This commit is contained in:
Ren Amamiya 2023-04-28 14:36:16 +07:00
parent a71502d19e
commit 87e8ee8954
44 changed files with 761 additions and 675 deletions

View File

@ -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"

View File

@ -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

View File

@ -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>
);
};
}

View File

@ -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>
</>
);
};
}

View File

@ -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>
);
};
}

View 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>
);
}

View File

@ -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 />,
};

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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>
);
};
}

View 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>
);
}

View 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>
);
}

View File

@ -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>
);
};
}

View 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>
);
}

View File

@ -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>
</>
);
};
}

View File

@ -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>

View File

@ -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';

View File

@ -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>
);
};

View File

@ -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>
)}
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};
}

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -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>
);
});

View File

@ -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>
);
};

View File

@ -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"

View File

@ -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>
);
});

View File

@ -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>
)}

View File

@ -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>
);
};
}

View File

@ -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>
);
};
}

View File

@ -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>
);
};
}

View File

@ -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>;
}

View File

@ -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>
);
});

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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);

View File

@ -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,
}
);

View File

@ -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,
};
};

View File

@ -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();