replace eslint/prettier with rome

This commit is contained in:
Ren Amamiya 2023-05-14 17:05:53 +07:00
parent 48d690d33a
commit 409a625dcc
154 changed files with 7639 additions and 8525 deletions

View File

@ -1,8 +0,0 @@
.git/
.vscode-test/
out/
dist/
test-fixtures/
node_modules/
src-tauri/

View File

@ -1,24 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"printWidth": 120,
"useTabs": false,
"endOfLine": "lf",
"bracketSpacing": true,
"bracketSameLine": false,
"importOrder": [
"^@app/(.*)$",
"^@shared/(.*)$",
"^@icons/(.*)$",
"^@stores/(.*)$",
"^@utils/(.*)$",
"<THIRD_PARTY_MODULES>",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": false
}

View File

@ -10,8 +10,7 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"lint-staged": { "lint-staged": {
"**/*": "prettier --write --ignore-unknown", "**/*.{js,ts,jsx,tsx}": "rome check --apply"
"**/*.{js,ts,jsx,tsx}": "eslint --fix"
}, },
"dependencies": { "dependencies": {
"@floating-ui/react": "^0.23.1", "@floating-ui/react": "^0.23.1",
@ -42,29 +41,20 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "^1.3.1", "@tauri-apps/cli": "^1.3.1",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/node": "^18.16.9", "@types/node": "^18.16.9",
"@types/react": "^18.2.6", "@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
"@types/youtube-player": "^5.5.7", "@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"@vitejs/plugin-react-swc": "^3.3.1", "@vitejs/plugin-react-swc": "^3.3.1",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"csstype": "^3.1.2", "csstype": "^3.1.2",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "^8.40.0",
"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.5",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^13.2.2", "lint-staged": "^13.2.2",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.2.8",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"rome": "12.1.0",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vite": "^4.3.5", "vite": "^4.3.5",

File diff suppressed because it is too large Load Diff

18
rome.json Normal file
View File

@ -0,0 +1,18 @@
{
"$schema": "https://docs.rome.tools/schemas/12.1.0/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"noSvgWithoutTitle": "off"
},
"suspicious": {
"noExplicitAny": "off"
}
}
}
}

View File

@ -1 +1 @@
export { LayoutOnboarding as Layout } from './layout'; export { LayoutOnboarding as Layout } from "./layout";

View File

@ -1,9 +1,9 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
export default function User({ pubkey }: { pubkey: string }) { export default function User({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
@ -20,8 +20,12 @@ export default function User({ pubkey }: { pubkey: string }) {
/> />
</div> </div>
<div className="flex w-full flex-1 flex-col items-start text-start"> <div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-zinc-200">{user?.display_name || user?.name}</span> <span className="truncate font-medium leading-tight text-zinc-200">
<span className="text-sm leading-tight text-zinc-400">{user?.nip05?.toLowerCase() || shortenKey(pubkey)}</span> {user?.display_name || user?.name}
</span>
<span className="text-sm leading-tight text-zinc-400">
{user?.nip05?.toLowerCase() || shortenKey(pubkey)}
</span>
</div> </div>
</div> </div>
); );

View File

@ -1,15 +1,15 @@
import ArrowLeftIcon from '@icons/arrowLeft'; import ArrowLeftIcon from "@icons/arrowLeft";
import ArrowRightIcon from '@icons/arrowRight'; import ArrowRightIcon from "@icons/arrowRight";
import useSWR from 'swr'; import useSWR from "swr";
const fetcher = async () => { const fetcher = async () => {
const { platform } = await import('@tauri-apps/api/os'); const { platform } = await import("@tauri-apps/api/os");
return await platform(); return await platform();
}; };
export function LayoutOnboarding({ children }: { children: React.ReactNode }) { export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
const { data: platform } = useSWR('platform', fetcher); const { data: platform } = useSWR("platform", fetcher);
const goBack = () => { const goBack = () => {
window.history.back(); window.history.back();
@ -26,19 +26,36 @@ export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
data-tauri-drag-region data-tauri-drag-region
className="relative h-11 shrink-0 border border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black" className="relative h-11 shrink-0 border border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
> >
<div data-tauri-drag-region className="flex h-full w-full flex-1 items-center px-2"> <div
<div className={`flex h-full items-center gap-2 ${platform === 'darwin' ? 'pl-[68px]' : ''}`}> data-tauri-drag-region
className="flex h-full w-full flex-1 items-center px-2"
>
<div
className={`flex h-full items-center gap-2 ${
platform === "darwin" ? "pl-[68px]" : ""
}`}
>
<button <button
type="button"
onClick={() => goBack()} onClick={() => goBack()}
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900" className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
> >
<ArrowLeftIcon width={16} height={16} className="text-zinc-500 group-hover:text-zinc-300" /> <ArrowLeftIcon
width={16}
height={16}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button> </button>
<button <button
type="button"
onClick={() => goForward()} onClick={() => goForward()}
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900" className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
> >
<ArrowRightIcon width={16} height={16} className="text-zinc-500 group-hover:text-zinc-300" /> <ArrowRightIcon
width={16}
height={16}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,15 +1,15 @@
import EyeOffIcon from '@icons/eyeOff'; import EyeOffIcon from "@icons/eyeOff";
import EyeOnIcon from '@icons/eyeOn'; import EyeOnIcon from "@icons/eyeOn";
import { onboardingAtom } from '@stores/onboarding'; import { onboardingAtom } from "@stores/onboarding";
import { useSetAtom } from 'jotai'; import { useSetAtom } from "jotai";
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools'; import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
import { useMemo, useState } from 'react'; import { useMemo, useState } from "react";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
export function Page() { export function Page() {
const [type, setType] = useState('password'); const [type, setType] = useState("password");
const setOnboarding = useSetAtom(onboardingAtom); const setOnboarding = useSetAtom(onboardingAtom);
const privkey = useMemo(() => generatePrivateKey(), []); const privkey = useMemo(() => generatePrivateKey(), []);
@ -19,27 +19,31 @@ export function Page() {
// toggle private key // toggle private key
const showPrivateKey = () => { const showPrivateKey = () => {
if (type === 'password') { if (type === "password") {
setType('text'); setType("text");
} else { } else {
setType('password'); setType("password");
} }
}; };
const submit = () => { const submit = () => {
setOnboarding((prev) => ({ ...prev, pubkey: pubkey, privkey: privkey })); setOnboarding((prev) => ({ ...prev, pubkey: pubkey, privkey: privkey }));
navigate('/auth/create/step-2'); navigate("/auth/create/step-2");
}; };
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-2xl font-semibold text-zinc-200">Lume is auto-generated key for you</h1> <h1 className="text-2xl font-semibold text-zinc-200">
Lume is auto-generated key for you
</h1>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">Public Key</label> <label className="text-sm font-semibold text-zinc-400">
Public Key
</label>
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20"> <div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<input <input
readOnly readOnly
@ -49,7 +53,9 @@ export function Page() {
</div> </div>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">Private Key</label> <label className="text-sm font-semibold text-zinc-400">
Private Key
</label>
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20"> <div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<input <input
readOnly readOnly
@ -58,13 +64,22 @@ export function Page() {
className="relative w-full rounded-lg border border-black/5 py-2.5 pl-3.5 pr-11 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600" className="relative w-full rounded-lg border border-black/5 py-2.5 pl-3.5 pr-11 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
/> />
<button <button
type="button"
onClick={() => showPrivateKey()} onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700" className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
> >
{type === 'password' ? ( {type === "password" ? (
<EyeOffIcon width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" /> <EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-200"
/>
) : ( ) : (
<EyeOnIcon width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" /> <EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-200"
/>
)} )}
</button> </button>
</div> </div>

View File

@ -1,13 +1,13 @@
import { AvatarUploader } from '@shared/avatarUploader'; import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
import { onboardingAtom } from '@stores/onboarding'; import { onboardingAtom } from "@stores/onboarding";
import { useAtom } from 'jotai'; import { useAtom } from "jotai";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useForm } from 'react-hook-form'; import { useForm } from "react-hook-form";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
export function Page() { export function Page() {
const [image, setImage] = useState(DEFAULT_AVATAR); const [image, setImage] = useState(DEFAULT_AVATAR);
@ -32,52 +32,70 @@ export function Page() {
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
setLoading(true); setLoading(true);
setOnboarding((prev) => ({ ...prev, metadata: data })); setOnboarding((prev) => ({ ...prev, metadata: data }));
navigate('/auth/create/step-3'); navigate("/auth/create/step-3");
}; };
useEffect(() => { useEffect(() => {
setValue('picture', image); setValue("picture", image);
}, [setValue, image]); }, [setValue, image]);
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-2xl font-semibold text-zinc-200">Create your profile</h1> <h1 className="text-2xl font-semibold text-zinc-200">
Create your profile
</h1>
</div> </div>
<div className="w-full rounded-lg border border-zinc-800 bg-zinc-900 p-4"> <div className="w-full rounded-lg border border-zinc-800 bg-zinc-900 p-4">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4"> <form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<input <input
type={'hidden'} type={"hidden"}
{...register('picture')} {...register("picture")}
value={image} value={image}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">Avatar</label> <label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">
Avatar
</label>
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950"> <div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
<Image src={image} alt="avatar" className="relative z-10 h-11 w-11 rounded-md" /> <Image
src={image}
alt="avatar"
className="relative z-10 h-11 w-11 rounded-md"
/>
<div className="absolute bottom-3 right-3 z-10"> <div className="absolute bottom-3 right-3 z-10">
<AvatarUploader valueState={setImage} /> <AvatarUploader valueState={setImage} />
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">Display Name *</label> <label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">
Display Name *
</label>
<div className="relative w-full shrink-0 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"> <div className="relative w-full shrink-0 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">
<input <input
type={'text'} type={"text"}
{...register('display_name', { required: true, minLength: 4 })} {...register("display_name", {
required: true,
minLength: 4,
})}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
</div> </div>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">About</label> <label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">
About
</label>
<div className="relative h-20 w-full shrink-0 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"> <div className="relative h-20 w-full shrink-0 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">
<textarea <textarea
{...register('about')} {...register("about")}
spellCheck={false} spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
@ -96,6 +114,7 @@ export function Page() {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<title id="loading">Loading</title>
<circle <circle
className="opacity-25" className="opacity-25"
cx="12" cx="12"
@ -103,12 +122,12 @@ export function Page() {
r="10" r="10"
stroke="currentColor" stroke="currentColor"
strokeWidth="4" strokeWidth="4"
></circle> />
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
<span>Continue </span> <span>Continue </span>

View File

@ -1,53 +1,117 @@
import User from '@app/auth/components/user'; import User from "@app/auth/components/user";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import CheckCircleIcon from '@icons/checkCircle'; import CheckCircleIcon from "@icons/checkCircle";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { onboardingAtom } from '@stores/onboarding'; import { onboardingAtom } from "@stores/onboarding";
import { createAccount, createPleb } from '@utils/storage'; import { createAccount, createPleb } from "@utils/storage";
import { arrayToNIP02 } from '@utils/transform'; import { arrayToNIP02 } from "@utils/transform";
import { useAtom } from 'jotai'; import { useAtom } from "jotai";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { useContext, useState } from 'react'; import { useContext, useState } from "react";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
const initialList = [ const initialList = [
{ pubkey: '82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2' }, {
{ pubkey: 'a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98' }, pubkey: "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
{ pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9' }, },
{ pubkey: 'c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0' }, {
{ pubkey: '6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93' }, pubkey: "a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98",
{ pubkey: 'e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411' }, },
{ pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d' }, {
{ pubkey: 'c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15' }, pubkey: "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
{ pubkey: 'e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42' }, },
{ pubkey: '84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240' }, {
{ pubkey: '703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898' }, pubkey: "c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0",
{ pubkey: 'bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce' }, },
{ pubkey: '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0' }, {
{ pubkey: 'c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965' }, pubkey: "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
{ pubkey: 'c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6' }, },
{ pubkey: '6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3' }, {
{ pubkey: '50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63' }, pubkey: "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411",
{ pubkey: '3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594' }, },
{ pubkey: '6e3f51664e19e082df5217fd4492bb96907405a0b27028671dd7f297b688608c' }, {
{ pubkey: '2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884' }, pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
{ pubkey: '3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24' }, },
{ pubkey: 'eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f' }, {
{ pubkey: 'be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479' }, pubkey: "c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15",
{ pubkey: 'a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f' }, },
{ pubkey: '1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b' }, {
{ pubkey: 'c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5' }, pubkey: "e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42",
{ pubkey: '460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c' }, },
{ pubkey: '7f3b464b9ff3623630485060cbda3a7790131c5339a7803bde8feb79a5e1b06a' }, {
{ pubkey: 'b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27' }, pubkey: "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
{ pubkey: 'e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2' }, },
{ pubkey: 'ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14' }, {
{ pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609' }, pubkey: "703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898",
},
{
pubkey: "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce",
},
{
pubkey: "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
},
{
pubkey: "c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965",
},
{
pubkey: "c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6",
},
{
pubkey: "6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3",
},
{
pubkey: "50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63",
},
{
pubkey: "3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594",
},
{
pubkey: "6e3f51664e19e082df5217fd4492bb96907405a0b27028671dd7f297b688608c",
},
{
pubkey: "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884",
},
{
pubkey: "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24",
},
{
pubkey: "eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f",
},
{
pubkey: "be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479",
},
{
pubkey: "a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f",
},
{
pubkey: "1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b",
},
{
pubkey: "c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5",
},
{
pubkey: "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
},
{
pubkey: "7f3b464b9ff3623630485060cbda3a7790131c5339a7803bde8feb79a5e1b06a",
},
{
pubkey: "b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27",
},
{
pubkey: "e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2",
},
{
pubkey: "ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14",
},
{
pubkey: "ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609",
},
]; ];
export function Page() { export function Page() {
@ -59,7 +123,9 @@ export function Page() {
// toggle follow state // toggle follow state
const toggleFollow = (pubkey: string) => { const toggleFollow = (pubkey: string) => {
const arr = follows.includes(pubkey) ? follows.filter((i) => i !== pubkey) : [...follows, pubkey]; const arr = follows.includes(pubkey)
? follows.filter((i) => i !== pubkey)
: [...follows, pubkey];
setFollows(arr); setFollows(arr);
}; };
@ -82,7 +148,7 @@ export function Page() {
const nip02 = arrayToNIP02(follows); const nip02 = arrayToNIP02(follows);
// build event // build event
const event: any = { const event: any = {
content: '', content: "",
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
kind: 3, kind: 3,
pubkey: onboarding.pubkey, pubkey: onboarding.pubkey,
@ -100,17 +166,26 @@ export function Page() {
const followsIncludeSelf = follows.concat([onboarding.pubkey]); const followsIncludeSelf = follows.concat([onboarding.pubkey]);
// insert to database // insert to database
createAccount(onboarding.pubkey, onboarding.privkey, onboarding.metadata, arrayToNIP02(followsIncludeSelf), 1) createAccount(
onboarding.pubkey,
onboarding.privkey,
onboarding.metadata,
arrayToNIP02(followsIncludeSelf),
1,
)
.then((res) => { .then((res) => {
if (res) { if (res) {
for (const tag of follows) { for (const tag of follows) {
fetch(`https://us.rbr.bio/${tag}/metadata.json`) fetch(`https://us.rbr.bio/${tag}/metadata.json`)
.then((data) => data.json()) .then((data) => data.json())
.then((data) => createPleb(tag, data ?? '')); .then((data) => createPleb(tag, data ?? ""));
} }
broadcastAccount(); broadcastAccount();
broadcastContacts(); broadcastContacts();
setTimeout(() => navigate('/', { overwriteLastHistoryEntry: true }), 2000); setTimeout(
() => navigate("/", { overwriteLastHistoryEntry: true }),
2000,
);
} else { } else {
console.error(); console.error();
} }
@ -122,7 +197,9 @@ export function Page() {
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-2xl font-semibold text-zinc-200">Personalized your newsfeed</h1> <h1 className="text-2xl font-semibold text-zinc-200">
Personalized your newsfeed
</h1>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="w-full rounded-lg border border-zinc-800 bg-zinc-900"> <div className="w-full rounded-lg border border-zinc-800 bg-zinc-900">
@ -130,13 +207,13 @@ export function Page() {
Follow at least Follow at least
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text font-bold text-transparent"> <span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text font-bold text-transparent">
{follows.length}/10 {follows.length}/10
</span>{' '} </span>{" "}
plebs plebs
</div> </div>
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2"> <div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
{initialList.map((item: { pubkey: string }, index: number) => ( {initialList.map((item: { pubkey: string }, index: number) => (
<button <button
key={index} key={`item-${index}`}
type="button" type="button"
onClick={() => toggleFollow(item.pubkey)} onClick={() => toggleFollow(item.pubkey)}
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1" className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
@ -144,7 +221,11 @@ export function Page() {
<User pubkey={item.pubkey} /> <User pubkey={item.pubkey} />
{follows.includes(item.pubkey) && ( {follows.includes(item.pubkey) && (
<div> <div>
<CheckCircleIcon width={16} height={16} className="text-green-400" /> <CheckCircleIcon
width={16}
height={16}
className="text-green-400"
/>
</div> </div>
)} )}
</button> </button>
@ -153,6 +234,7 @@ export function Page() {
</div> </div>
{follows.length >= 10 && ( {follows.length >= 10 && (
<button <button
type="button"
onClick={() => submit()} onClick={() => submit()}
className="inline-flex h-10 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 px-3.5 font-medium text-white shadow-button hover:bg-fuchsia-600 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-70" className="inline-flex h-10 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 px-3.5 font-medium text-white shadow-button hover:bg-fuchsia-600 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-70"
> >
@ -163,12 +245,20 @@ export function Page() {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
<span>Continue </span> <span>Continue </span>

View File

@ -1,9 +1,9 @@
import { onboardingAtom } from '@stores/onboarding'; import { onboardingAtom } from "@stores/onboarding";
import { useSetAtom } from 'jotai'; import { useSetAtom } from "jotai";
import { getPublicKey, nip19 } from 'nostr-tools'; import { getPublicKey, nip19 } from "nostr-tools";
import { Resolver, useForm } from 'react-hook-form'; import { Resolver, useForm } from "react-hook-form";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
type FormValues = { type FormValues = {
key: string; key: string;
@ -15,8 +15,8 @@ const resolver: Resolver<FormValues> = async (values) => {
errors: !values.key errors: !values.key
? { ? {
key: { key: {
type: 'required', type: "required",
message: 'This is required.', message: "This is required.",
}, },
} }
: {}, : {},
@ -35,20 +35,20 @@ export function Page() {
const onSubmit = async (data: any) => { const onSubmit = async (data: any) => {
try { try {
let privkey = data['key']; let privkey = data["key"];
if (privkey.substring(0, 4) === 'nsec') { if (privkey.substring(0, 4) === "nsec") {
privkey = nip19.decode(privkey).data; privkey = nip19.decode(privkey).data;
} }
if (typeof getPublicKey(privkey) === 'string') { if (typeof getPublicKey(privkey) === "string") {
setOnboardingPrivkey((prev) => ({ ...prev, privkey: privkey })); setOnboardingPrivkey((prev) => ({ ...prev, privkey: privkey }));
navigate(`/auth/import/step-2`); navigate("/auth/import/step-2");
} }
} catch (error) { } catch (error) {
setError('key', { setError("key", {
type: 'custom', type: "custom",
message: 'Private Key is invalid, please check again', message: "Private Key is invalid, please check again",
}); });
} }
}; };
@ -57,12 +57,17 @@ export function Page() {
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-2xl font-semibold text-zinc-200">Import your key</h1> <h1 className="text-2xl font-semibold text-zinc-200">
Import your key
</h1>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
{/* #TODO: add function */} {/* #TODO: add function */}
<button className="inline-flex w-full transform items-center justify-center gap-1.5 rounded-lg bg-zinc-900 px-3.5 py-2.5 font-medium text-zinc-400 active:translate-y-1"> <button
type="button"
className="inline-flex w-full transform items-center justify-center gap-1.5 rounded-lg bg-zinc-900 px-3.5 py-2.5 font-medium text-zinc-400 active:translate-y-1"
>
<div className="inline-flex items-center rounded-md bg-zinc-400/10 px-2 py-0.5 text-xs font-medium ring-1 ring-inset ring-zinc-400/20"> <div className="inline-flex items-center rounded-md bg-zinc-400/10 px-2 py-0.5 text-xs font-medium ring-1 ring-inset ring-zinc-400/20">
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
Coming soon Coming soon
@ -73,23 +78,28 @@ export function Page() {
</div> </div>
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-zinc-800"></div> <div className="w-full border-t border-zinc-800" />
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<span className="bg-zinc-950 px-2 text-sm text-zinc-500">or</span> <span className="bg-zinc-950 px-2 text-sm text-zinc-500">or</span>
</div> </div>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3"> <form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3"
>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] 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"> <div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] 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">
<input <input
{...register('key', { required: true, minLength: 32 })} {...register("key", { required: true, minLength: 32 })}
type={'password'} type={"password"}
placeholder="Paste private key here..." placeholder="Paste private key here..."
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 text-center 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" className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 text-center 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> </div>
<span className="text-xs text-red-400">{errors.key && <p>{errors.key.message}</p>}</span> <span className="text-xs text-red-400">
{errors.key && <p>{errors.key.message}</p>}
</span>
</div> </div>
<div className="flex h-9 items-center justify-center"> <div className="flex h-9 items-center justify-center">
{isSubmitting ? ( {isSubmitting ? (
@ -99,12 +109,20 @@ export function Page() {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
<button <button

View File

@ -1,26 +1,31 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { DEFAULT_AVATAR, READONLY_RELAYS } from '@stores/constants'; import { DEFAULT_AVATAR, READONLY_RELAYS } from "@stores/constants";
import { onboardingAtom } from '@stores/onboarding'; import { onboardingAtom } from "@stores/onboarding";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import { createAccount, createPleb } from '@utils/storage'; import { createAccount, createPleb } from "@utils/storage";
import { useAtom } from 'jotai'; import { useAtom } from "jotai";
import { getPublicKey } from 'nostr-tools'; import { getPublicKey } from "nostr-tools";
import { useContext, useMemo, useState } from 'react'; import { useContext, useMemo, useState } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [onboarding, setOnboarding] = useAtom(onboardingAtom); const [onboarding, setOnboarding] = useAtom(onboardingAtom);
const pubkey = useMemo(() => (onboarding.privkey ? getPublicKey(onboarding.privkey) : ''), [onboarding.privkey]); const pubkey = useMemo(
() => (onboarding.privkey ? getPublicKey(onboarding.privkey) : ""),
[onboarding.privkey],
);
const { data, error } = useSWRSubscription(pubkey ? pubkey : null, (key, { next }) => { const { data, error } = useSWRSubscription(
pubkey ? pubkey : null,
(key, { next }) => {
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
@ -43,19 +48,20 @@ export function Page() {
default: default:
break; break;
} }
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
const submit = () => { const submit = () => {
// show loading indicator // show loading indicator
setLoading(true); setLoading(true);
const follows = onboarding.follows.concat([['p', pubkey]]); const follows = onboarding.follows.concat([["p", pubkey]]);
// insert to database // insert to database
createAccount(pubkey, onboarding.privkey, onboarding.metadata, follows, 1) createAccount(pubkey, onboarding.privkey, onboarding.metadata, follows, 1)
.then((res) => { .then((res) => {
@ -63,9 +69,12 @@ export function Page() {
for (const tag of onboarding.follows) { for (const tag of onboarding.follows) {
fetch(`https://us.rbr.bio/${tag[1]}/metadata.json`) fetch(`https://us.rbr.bio/${tag[1]}/metadata.json`)
.then((data) => data.json()) .then((data) => data.json())
.then((data) => createPleb(tag[1], data ?? '')); .then((data) => createPleb(tag[1], data ?? ""));
} }
setTimeout(() => navigate('/', { overwriteLastHistoryEntry: true }), 2000); setTimeout(
() => navigate("/", { overwriteLastHistoryEntry: true }),
2000,
);
} else { } else {
console.error(); console.error();
} }
@ -77,17 +86,19 @@ export function Page() {
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-2xl font-semibold">{loading ? 'Creating...' : 'Continue with'}</h1> <h1 className="text-2xl font-semibold">
{loading ? "Creating..." : "Continue with"}
</h1>
</div> </div>
<div className="w-full rounded-lg border border-zinc-800 bg-zinc-900 p-4"> <div className="w-full rounded-lg border border-zinc-800 bg-zinc-900 p-4">
{error && <div>Failed to load profile</div>} {error && <div>Failed to load profile</div>}
{!data ? ( {!data ? (
<div className="w-full"> <div className="w-full">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800"></div> <div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
<div> <div>
<h3 className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800"></h3> <h3 className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" />
<p className="h-3 w-36 animate-pulse rounded bg-zinc-800"></p> <p className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
</div> </div>
</div> </div>
</div> </div>
@ -100,8 +111,12 @@ export function Page() {
alt={pubkey} alt={pubkey}
/> />
<div> <div>
<h3 className="font-medium leading-none text-zinc-200">{data.display_name || data.name}</h3> <h3 className="font-medium leading-none text-zinc-200">
<p className="text-sm text-zinc-400">{data.nip05 || shortenKey(pubkey)}</p> {data.display_name || data.name}
</h3>
<p className="text-sm text-zinc-400">
{data.nip05 || shortenKey(pubkey)}
</p>
</div> </div>
</div> </div>
<button <button
@ -123,12 +138,12 @@ export function Page() {
r="10" r="10"
stroke="currentColor" stroke="currentColor"
strokeWidth="4" strokeWidth="4"
></circle> />
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
<span>Continue </span> <span>Continue </span>

View File

@ -1,48 +1,48 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import ArrowRightIcon from '@icons/arrowRight'; import ArrowRightIcon from "@icons/arrowRight";
const PLEBS = [ const PLEBS = [
'https://133332.xyz/p.jpg', "https://133332.xyz/p.jpg",
'https://void.cat/d/3Bp6jSHURFNQ9u3pK8nwtq.webp', "https://void.cat/d/3Bp6jSHURFNQ9u3pK8nwtq.webp",
'https://i.imgur.com/f8SyhRL.jpg', "https://i.imgur.com/f8SyhRL.jpg",
'http://nostr.build/i/6369.jpg', "http://nostr.build/i/6369.jpg",
'https://pbs.twimg.com/profile_images/1622010345589190656/mAPqsmtz_400x400.jpg', "https://pbs.twimg.com/profile_images/1622010345589190656/mAPqsmtz_400x400.jpg",
'https://media.tenor.com/l5arkXy9RfIAAAAd/thunder.gif', "https://media.tenor.com/l5arkXy9RfIAAAAd/thunder.gif",
'https://nostr.build/i/p/nostr.build_0e412058980ed2ac4adf3de639304c9e970e2745ba9ca19c75f984f4f6da4971.jpeg', "https://nostr.build/i/p/nostr.build_0e412058980ed2ac4adf3de639304c9e970e2745ba9ca19c75f984f4f6da4971.jpeg",
'https://nostr.build/i/nostr.build_864a019a6c1d3a90a17363553d32b71de618d250f02cf0a59ca19fb3029fd5bc.jpg', "https://nostr.build/i/nostr.build_864a019a6c1d3a90a17363553d32b71de618d250f02cf0a59ca19fb3029fd5bc.jpg",
'https://void.cat/d/8zE9T8a39YfUVjrLM4xcpE.webp', "https://void.cat/d/8zE9T8a39YfUVjrLM4xcpE.webp",
'https://avatars.githubusercontent.com/u/89577423', "https://avatars.githubusercontent.com/u/89577423",
'https://pbs.twimg.com/profile_images/1363180486080663554/iN-r_BiM_400x400.jpg', "https://pbs.twimg.com/profile_images/1363180486080663554/iN-r_BiM_400x400.jpg",
'https://void.cat/d/JUBBqXgCcGBEh7jUgJaayy', "https://void.cat/d/JUBBqXgCcGBEh7jUgJaayy",
'https://phase1.attract-eu.com/wp-content/uploads/2020/03/ATTRACT_HPLM.png', "https://phase1.attract-eu.com/wp-content/uploads/2020/03/ATTRACT_HPLM.png",
'https://www.retro-synthwave.com/wp-content/uploads/2017/01/PowerGlove-23.jpg', "https://www.retro-synthwave.com/wp-content/uploads/2017/01/PowerGlove-23.jpg",
'https://void.cat/d/KvAEMvYNmy1rfCH6a7HZzh.webp', "https://void.cat/d/KvAEMvYNmy1rfCH6a7HZzh.webp",
'https://media.giphy.com/media/NqfMNCkyGwtXhKFlCR/giphy-downsized-large.gif', "https://media.giphy.com/media/NqfMNCkyGwtXhKFlCR/giphy-downsized-large.gif",
'https://i.imgur.com/VGpUNFS.jpg', "https://i.imgur.com/VGpUNFS.jpg",
'https://nostr.build/i/p/nostr.build_b39254db43d5557df99d1eb516f1c2f56a21a01b10c248f6eb66aa827c9a90f4.jpeg', "https://nostr.build/i/p/nostr.build_b39254db43d5557df99d1eb516f1c2f56a21a01b10c248f6eb66aa827c9a90f4.jpeg",
'https://davidcoen.it/wp-content/uploads/2020/11/7004972-taglio.jpg', "https://davidcoen.it/wp-content/uploads/2020/11/7004972-taglio.jpg",
'https://pbs.twimg.com/profile_images/1570432066348515330/26PtCuwF_400x400.jpg', "https://pbs.twimg.com/profile_images/1570432066348515330/26PtCuwF_400x400.jpg",
'https://nostr.build/i/nostr.build_9d33ee801aa08955be174554832952ab95a65d5e015176834c8aa9a4e2f2e3a5.jpg', "https://nostr.build/i/nostr.build_9d33ee801aa08955be174554832952ab95a65d5e015176834c8aa9a4e2f2e3a5.jpg",
'https://www.linkpicture.com/q/0FE78CFF-C931-4568-A7AA-DD8AEE889992.jpeg', "https://www.linkpicture.com/q/0FE78CFF-C931-4568-A7AA-DD8AEE889992.jpeg",
'https://nostr.build/i/nostr.build_97d6e2d25dd92422eb3d6d645b7cee9ed9c614f331be7e6f7db9ccfdbc5ee260.png', "https://nostr.build/i/nostr.build_97d6e2d25dd92422eb3d6d645b7cee9ed9c614f331be7e6f7db9ccfdbc5ee260.png",
'https://pbs.twimg.com/profile_images/1569570198348337152/-n1KD74u_400x400.jpg', "https://pbs.twimg.com/profile_images/1569570198348337152/-n1KD74u_400x400.jpg",
'https://pbs.twimg.com/profile_images/1600149653898596354/5PVe-r-J_400x400.jpg', "https://pbs.twimg.com/profile_images/1600149653898596354/5PVe-r-J_400x400.jpg",
'https://pbs.twimg.com/profile_images/1639659216372658178/Dnn-Ysp-_400x400.jpg', "https://pbs.twimg.com/profile_images/1639659216372658178/Dnn-Ysp-_400x400.jpg",
'https://pbs.twimg.com/profile_images/1554429112978120706/yr1hXl6R_400x400.jpg', "https://pbs.twimg.com/profile_images/1554429112978120706/yr1hXl6R_400x400.jpg",
'https://pbs.twimg.com/profile_images/1615478486688272385/q2ECeZDX_400x400.jpg', "https://pbs.twimg.com/profile_images/1615478486688272385/q2ECeZDX_400x400.jpg",
'https://pbs.twimg.com/profile_images/1638644441773748226/tNsA6RpG_400x400.jpg', "https://pbs.twimg.com/profile_images/1638644441773748226/tNsA6RpG_400x400.jpg",
'https://pbs.twimg.com/profile_images/1607882836740120576/3Tg1mTYJ_400x400.jpg', "https://pbs.twimg.com/profile_images/1607882836740120576/3Tg1mTYJ_400x400.jpg",
'https://pbs.twimg.com/profile_images/1401907430339002369/WKrP9Esn_400x400.jpg', "https://pbs.twimg.com/profile_images/1401907430339002369/WKrP9Esn_400x400.jpg",
'https://pbs.twimg.com/profile_images/1523971278478131200/TMPzfvhE_400x400.jpg', "https://pbs.twimg.com/profile_images/1523971278478131200/TMPzfvhE_400x400.jpg",
'https://pbs.twimg.com/profile_images/1626421539884204032/aj4tmzsk_400x400.png', "https://pbs.twimg.com/profile_images/1626421539884204032/aj4tmzsk_400x400.png",
'https://pbs.twimg.com/profile_images/1582771691779985408/C9MHYIgt_400x400.jpg', "https://pbs.twimg.com/profile_images/1582771691779985408/C9MHYIgt_400x400.jpg",
'https://pbs.twimg.com/profile_images/1409612480465276931/38Vyx4e8_400x400.jpg', "https://pbs.twimg.com/profile_images/1409612480465276931/38Vyx4e8_400x400.jpg",
'https://pbs.twimg.com/profile_images/1549826566787588098/MlduJCZO_400x400.jpg', "https://pbs.twimg.com/profile_images/1549826566787588098/MlduJCZO_400x400.jpg",
'https://pbs.twimg.com/profile_images/539211568035004416/sBMjPR9q_400x400.jpeg', "https://pbs.twimg.com/profile_images/539211568035004416/sBMjPR9q_400x400.jpeg",
'https://pbs.twimg.com/profile_images/1548660003522887682/1QMHmles_400x400.jpg', "https://pbs.twimg.com/profile_images/1548660003522887682/1QMHmles_400x400.jpg",
'https://pbs.twimg.com/profile_images/1362497143999787013/KLUoN1Vn_400x400.png', "https://pbs.twimg.com/profile_images/1362497143999787013/KLUoN1Vn_400x400.png",
'https://pbs.twimg.com/profile_images/1600434913240563713/AssmMGwf_400x400.jpg', "https://pbs.twimg.com/profile_images/1600434913240563713/AssmMGwf_400x400.jpg",
]; ];
const DURATION = 50000; const DURATION = 50000;
@ -52,17 +52,21 @@ const PLEBS_PER_ROW = 20;
const random = (min, max) => Math.floor(Math.random() * (max - min)) + min; const random = (min, max) => Math.floor(Math.random() * (max - min)) + min;
const shuffle = (arr) => [...arr].sort(() => 0.5 - Math.random()); const shuffle = (arr) => [...arr].sort(() => 0.5 - Math.random());
const InfiniteLoopSlider = ({ children, duration, reverse }: { children: any; duration: any; reverse: any }) => { const InfiniteLoopSlider = ({
children,
duration,
reverse,
}: { children: any; duration: any; reverse: any }) => {
return ( return (
<div> <div>
<div <div
className="flex w-fit" className="flex w-fit"
style={{ style={{
animationName: 'loop', animationName: "loop",
animationIterationCount: 'infinite', animationIterationCount: "infinite",
animationDirection: reverse ? 'reverse' : 'normal', animationDirection: reverse ? "reverse" : "normal",
animationDuration: duration + 'ms', animationDuration: `${duration}ms`,
animationTimingFunction: 'linear', animationTimingFunction: "linear",
}} }}
> >
{children} {children}
@ -78,12 +82,23 @@ export function Page() {
<div className="row-span-3 overflow-hidden"> <div className="row-span-3 overflow-hidden">
<div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4"> <div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4">
{[...new Array(ROWS)].map((_, i) => ( {[...new Array(ROWS)].map((_, i) => (
<InfiniteLoopSlider key={i} duration={random(DURATION - 5000, DURATION + 20000)} reverse={i % 2}> <InfiniteLoopSlider
key={`item-${i}`}
duration={random(DURATION - 5000, DURATION + 20000)}
reverse={i % 2}
>
{shuffle(PLEBS) {shuffle(PLEBS)
.slice(0, PLEBS_PER_ROW) .slice(0, PLEBS_PER_ROW)
.map((tag) => ( .map((tag) => (
<div key={tag} className="relative mr-4 h-11 w-11 gap-2 rounded-md bg-zinc-900 shadow-xl"> <div
<Image src={tag} alt={tag} className="h-11 w-11 rounded-md border border-zinc-900" /> key={tag}
className="relative mr-4 h-11 w-11 gap-2 rounded-md bg-zinc-900 shadow-xl"
>
<Image
src={tag}
alt={tag}
className="h-11 w-11 rounded-md border border-zinc-900"
/>
</div> </div>
))} ))}
</InfiniteLoopSlider> </InfiniteLoopSlider>

View File

@ -1 +1 @@
export { LayoutChannel as Layout } from './layout'; export { LayoutChannel as Layout } from "./layout";

View File

@ -1,9 +1,9 @@
import MutedItem from '@app/channel/components/mutedItem'; import MutedItem from "@app/channel/components/mutedItem";
import MuteIcon from '@icons/mute'; import MuteIcon from "@icons/mute";
import { Popover, Transition } from '@headlessui/react'; import { Popover, Transition } from "@headlessui/react";
import { Fragment } from 'react'; import { Fragment } from "react";
export default function ChannelBlackList({ blacklist }: { blacklist: any }) { export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
return ( return (
@ -12,10 +12,16 @@ export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
<> <>
<Popover.Button <Popover.Button
className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 focus:outline-none ${ className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 focus:outline-none ${
open ? 'bg-zinc-800 hover:bg-zinc-700' : 'bg-zinc-900 hover:bg-zinc-800' open
? "bg-zinc-800 hover:bg-zinc-700"
: "bg-zinc-900 hover:bg-zinc-800"
}`} }`}
> >
<MuteIcon width={16} height={16} className="text-zinc-400 group-hover:text-zinc-200" /> <MuteIcon
width={16}
height={16}
className="text-zinc-400 group-hover:text-zinc-200"
/>
</Popover.Button> </Popover.Button>
<Transition <Transition
as={Fragment} as={Fragment}
@ -34,7 +40,8 @@ export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
Your muted list Your muted list
</h3> </h3>
<p className="text-xs leading-tight text-zinc-400"> <p className="text-xs leading-tight text-zinc-400">
Currently, unmute only affect locally, when you move to new client, muted list will loaded again Currently, unmute only affect locally, when you move to
new client, muted list will loaded again
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,22 +1,22 @@
import { AvatarUploader } from '@shared/avatarUploader'; import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import CancelIcon from '@icons/cancel'; import CancelIcon from "@icons/cancel";
import PlusIcon from '@icons/plus'; import PlusIcon from "@icons/plus";
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from '@stores/constants'; import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { createChannel } from '@utils/storage'; import { createChannel } from "@utils/storage";
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from 'react'; import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from 'react-hook-form'; import { useForm } from "react-hook-form";
import { useSWRConfig } from 'swr'; import { useSWRConfig } from "swr";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
export default function ChannelCreateModal() { export default function ChannelCreateModal() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -63,7 +63,7 @@ export default function ChannelCreateModal() {
// insert to database // insert to database
createChannel(event.id, event.pubkey, event.content, event.created_at); createChannel(event.id, event.pubkey, event.content, event.created_at);
// update channe llist // update channe llist
mutate('channels'); mutate("channels");
// reset form // reset form
reset(); reset();
setTimeout(() => { setTimeout(() => {
@ -73,12 +73,12 @@ export default function ChannelCreateModal() {
navigate(`/app/channel?id=${event.id}`); navigate(`/app/channel?id=${event.id}`);
}, 2000); }, 2000);
} else { } else {
console.log('error'); console.log("error");
} }
}; };
useEffect(() => { useEffect(() => {
setValue('picture', image); setValue("picture", image);
}, [setValue, image]); }, [setValue, image]);
return ( return (
@ -92,7 +92,9 @@ export default function ChannelCreateModal() {
<PlusIcon width={12} height={12} className="text-zinc-500" /> <PlusIcon width={12} height={12} className="text-zinc-500" />
</div> </div>
<div> <div>
<h5 className="text-[13px] font-semibold text-zinc-500 group-hover:text-zinc-400">Add a new channel</h5> <h5 className="text-[13px] font-semibold text-zinc-500 group-hover:text-zinc-400">
Add a new channel
</h5>
</div> </div>
</button> </button>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
@ -131,30 +133,42 @@ export default function ChannelCreateModal() {
<button <button
type="button" type="button"
onClick={closeModal} onClick={closeModal}
autoFocus={false}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon width={20} height={20} className="text-zinc-300" /> <CancelIcon
width={20}
height={20}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="leading-tight text-zinc-400"> <Dialog.Description className="leading-tight text-zinc-400">
Channels are freedom square, everyone can speech freely, no one can stop you or deceive what to Channels are freedom square, everyone can speech freely,
speech no one can stop you or deceive what to speech
</Dialog.Description> </Dialog.Description>
</div> </div>
</div> </div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3"> <div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form onSubmit={handleSubmit(onSubmit)} className="flex h-full w-full flex-col gap-4"> <form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4"
>
<input <input
type={'hidden'} type={"hidden"}
{...register('picture')} {...register("picture")}
value={image} value={image}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">Picture</label> <label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">
Picture
</label>
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950"> <div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
<Image src={image} alt="channel picture" className="relative z-10 h-11 w-11 rounded-md" /> <Image
src={image}
alt="channel picture"
className="relative z-10 h-11 w-11 rounded-md"
/>
<div className="absolute bottom-3 right-3 z-10"> <div className="absolute bottom-3 right-3 z-10">
<AvatarUploader valueState={setImage} /> <AvatarUploader valueState={setImage} />
</div> </div>
@ -166,8 +180,11 @@ export default function ChannelCreateModal() {
</label> </label>
<div className="relative w-full shrink-0 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"> <div className="relative w-full shrink-0 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">
<input <input
type={'text'} type={"text"}
{...register('name', { required: true, minLength: 4 })} {...register("name", {
required: true,
minLength: 4,
})}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
@ -179,7 +196,7 @@ export default function ChannelCreateModal() {
</label> </label>
<div className="relative h-20 w-full shrink-0 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"> <div className="relative h-20 w-full shrink-0 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">
<textarea <textarea
{...register('about')} {...register("about")}
spellCheck={false} spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
@ -188,7 +205,9 @@ export default function ChannelCreateModal() {
<div className="flex h-14 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2"> <div className="flex h-14 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<div className="inline-flex items-center gap-1"> <div className="inline-flex items-center gap-1">
<span className="text-sm font-bold leading-none text-zinc-200">Make Private</span> <span className="text-sm font-bold leading-none text-zinc-200">
Make Private
</span>
<div className="inline-flex items-center rounded-md bg-zinc-400/10 px-2 py-0.5 text-xs font-medium ring-1 ring-inset ring-zinc-400/20"> <div className="inline-flex items-center rounded-md bg-zinc-400/10 px-2 py-0.5 text-xs font-medium ring-1 ring-inset ring-zinc-400/20">
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
Coming soon Coming soon
@ -201,12 +220,13 @@ export default function ChannelCreateModal() {
</div> </div>
<div> <div>
<button <button
type="button"
disabled disabled
className="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent bg-zinc-900 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-600 focus:ring-offset-2" className="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent bg-zinc-900 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-600 focus:ring-offset-2"
role="switch" role="switch"
aria-checked="false" aria-checked="false"
> >
<span className="pointer-events-none inline-block h-5 w-5 translate-x-0 transform rounded-full bg-zinc-600 shadow ring-0 transition duration-200 ease-in-out"></span> <span className="pointer-events-none inline-block h-5 w-5 translate-x-0 transform rounded-full bg-zinc-600 shadow ring-0 transition duration-200 ease-in-out" />
</button> </button>
</div> </div>
</div> </div>
@ -223,6 +243,7 @@ export default function ChannelCreateModal() {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<title id="loading">Loading</title>
<circle <circle
className="opacity-25" className="opacity-25"
cx="12" cx="12"
@ -230,15 +251,15 @@ export default function ChannelCreateModal() {
r="10" r="10"
stroke="currentColor" stroke="currentColor"
strokeWidth="4" strokeWidth="4"
></circle> />
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
'Create channel' "Create channel"
)} )}
</button> </button>
</div> </div>

View File

@ -1,8 +1,8 @@
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile'; import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
import { usePageContext } from '@utils/hooks/usePageContext'; import { usePageContext } from "@utils/hooks/usePageContext";
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export default function ChannelsListItem({ data }: { data: any }) { export default function ChannelsListItem({ data }: { data: any }) {
const channel: any = useChannelProfile(data.event_id, data.pubkey); const channel: any = useChannelProfile(data.event_id, data.pubkey);
@ -15,20 +15,26 @@ export default function ChannelsListItem({ data }: { data: any }) {
<a <a
href={`/app/channel?id=${data.event_id}&channelpub=${data.pubkey}`} href={`/app/channel?id=${data.event_id}&channelpub=${data.pubkey}`}
className={twMerge( className={twMerge(
'group inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900', "group inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900",
pageID === data.event_id ? 'dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800' : '' pageID === data.event_id
? "dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
: "",
)} )}
> >
<div <div
className={twMerge( className={twMerge(
'inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800', "inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800",
pageID === data.event_id ? 'dark:bg-zinc-800 group-hover:dark:bg-zinc-700' : '' pageID === data.event_id
? "dark:bg-zinc-800 group-hover:dark:bg-zinc-700"
: "",
)} )}
> >
<span className="text-xs text-zinc-200">#</span> <span className="text-xs text-zinc-200">#</span>
</div> </div>
<div> <div>
<h5 className="truncate text-[13px] font-semibold text-zinc-400">{channel?.name}</h5> <h5 className="truncate text-[13px] font-semibold text-zinc-400">
{channel?.name}
</h5>
</div> </div>
</a> </a>
); );

View File

@ -1,30 +1,32 @@
import ChannelCreateModal from '@app/channel/components/createModal'; import ChannelCreateModal from "@app/channel/components/createModal";
import ChannelsListItem from '@app/channel/components/item'; import ChannelsListItem from "@app/channel/components/item";
import { getChannels } from '@utils/storage'; import { getChannels } from "@utils/storage";
import useSWR from 'swr'; import useSWR from "swr";
const fetcher = () => getChannels(10, 0); const fetcher = () => getChannels(10, 0);
export default function ChannelsList() { export default function ChannelsList() {
const { data, error }: any = useSWR('channels', fetcher); const { data, error }: any = useSWR("channels", fetcher);
return ( return (
<div className="flex flex-col gap-px"> <div className="flex flex-col gap-px">
{!data || error ? ( {!data || error ? (
<> <>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800"></div> <div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800"></div> <div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
</> </>
) : ( ) : (
data.map((item: { event_id: string }) => <ChannelsListItem key={item.event_id} data={item} />) data.map((item: { event_id: string }) => (
<ChannelsListItem key={item.event_id} data={item} />
))
)} )}
<ChannelCreateModal /> <ChannelCreateModal />
</div> </div>

View File

@ -1,8 +1,8 @@
import MiniMember from '@app/channel/components/miniMember'; import MiniMember from "@app/channel/components/miniMember";
import { channelMembersAtom } from '@stores/channel'; import { channelMembersAtom } from "@stores/channel";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
export default function ChannelMembers() { export default function ChannelMembers() {
const membersAsSet = useAtomValue(channelMembersAtom); const membersAsSet = useAtomValue(channelMembersAtom);
@ -10,26 +10,30 @@ export default function ChannelMembers() {
const miniMembersList = membersAsArray.slice(0, 4); const miniMembersList = membersAsArray.slice(0, 4);
const totalMembers = const totalMembers =
membersAsArray.length > 0 membersAsArray.length > 0
? '+' + ? `+${Intl.NumberFormat("en-US", {
Intl.NumberFormat('en-US', { notation: "compact",
notation: 'compact',
maximumFractionDigits: 1, maximumFractionDigits: 1,
}).format(membersAsArray.length) }).format(membersAsArray.length)}`
: 0; : 0;
return ( return (
<div> <div>
<div className="group flex -space-x-2 overflow-hidden hover:-space-x-1"> <div className="group flex -space-x-2 overflow-hidden hover:-space-x-1">
{miniMembersList.map((member, index) => ( {miniMembersList.map((member, index) => (
<MiniMember key={index} pubkey={member} /> <MiniMember key={`item-${index}`} pubkey={member} />
))} ))}
{totalMembers ? ( {totalMembers ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 ring-2 ring-zinc-950 transition-all duration-150 ease-in-out group-hover:bg-zinc-800"> <div className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 ring-2 ring-zinc-950 transition-all duration-150 ease-in-out group-hover:bg-zinc-800">
<span className="text-xs font-medium text-zinc-400 group-hover:text-zinc-200">{totalMembers}</span> <span className="text-xs font-medium text-zinc-400 group-hover:text-zinc-200">
{totalMembers}
</span>
</div> </div>
) : ( ) : (
<div> <div>
<button className="inline-flex h-8 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm text-white shadow-button"> <button
type="button"
className="inline-flex h-8 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm text-white shadow-button"
>
Invite Invite
</button> </button>
</div> </div>

View File

@ -1,12 +1,12 @@
import ChannelMessageItem from '@app/channel/components/messages/item'; import ChannelMessageItem from "@app/channel/components/messages/item";
import { sortedChannelMessagesAtom } from '@stores/channel'; import { sortedChannelMessagesAtom } from "@stores/channel";
import { getHourAgo } from '@utils/date'; import { getHourAgo } from "@utils/date";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from "react";
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from "react-virtuoso";
export default function ChannelMessageList() { export default function ChannelMessageList() {
const now = useRef(new Date()); const now = useRef(new Date());
@ -17,14 +17,14 @@ export default function ChannelMessageList() {
(index: string | number) => { (index: string | number) => {
return <ChannelMessageItem data={data[index]} />; return <ChannelMessageItem data={data[index]} />;
}, },
[data] [data],
); );
const computeItemKey = useCallback( const computeItemKey = useCallback(
(index: string | number) => { (index: string | number) => {
return data[index].id; return data[index].id;
}, },
[data] [data],
); );
return ( return (
@ -36,16 +36,19 @@ export default function ChannelMessageList() {
components={{ components={{
Header: () => ( Header: () => (
<div className="relative py-4"> <div className="relative py-4">
<div className="absolute inset-0 flex items-center" aria-hidden="true"> <div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-zinc-800" /> <div className="w-full border-t border-zinc-800" />
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-xs font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800"> <div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-xs font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
{getHourAgo(24, now.current).toLocaleDateString('en-US', { {getHourAgo(24, now.current).toLocaleDateString("en-US", {
weekday: 'long', weekday: "long",
year: 'numeric', year: "numeric",
month: 'long', month: "long",
day: 'numeric', day: "numeric",
})} })}
</div> </div>
</div> </div>
@ -53,8 +56,12 @@ export default function ChannelMessageList() {
), ),
EmptyPlaceholder: () => ( EmptyPlaceholder: () => (
<div className="flex flex-col gap-1 text-center"> <div className="flex flex-col gap-1 text-center">
<h3 className="text-sm font-semibold leading-none text-zinc-200">Nothing to see here yet</h3> <h3 className="text-sm font-semibold leading-none text-zinc-200">
<p className="text-sm leading-none text-zinc-400">Be the first to share a message in this channel.</p> Nothing to see here yet
</h3>
<p className="text-sm leading-none text-zinc-400">
Be the first to share a message in this channel.
</p>
</div> </div>
), ),
}} }}

View File

@ -1,22 +1,24 @@
import UserReply from '@app/channel/components/messages/userReply'; import UserReply from "@app/channel/components/messages/userReply";
import { ImagePicker } from '@shared/form/imagePicker'; import { ImagePicker } from "@shared/form/imagePicker";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import CancelIcon from '@icons/cancel'; import CancelIcon from "@icons/cancel";
import { channelContentAtom, channelReplyAtom } from '@stores/channel'; import { channelContentAtom, channelReplyAtom } from "@stores/channel";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from "jotai";
import { useResetAtom } from 'jotai/utils'; import { useResetAtom } from "jotai/utils";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { useContext } from 'react'; import { useContext } from "react";
export default function ChannelMessageForm({ channelID }: { channelID: string | string[] }) { export default function ChannelMessageForm({
channelID,
}: { channelID: string | string[] }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
@ -31,12 +33,12 @@ export default function ChannelMessageForm({ channelID }: { channelID: string |
if (channelReply.id !== null) { if (channelReply.id !== null) {
tags = [ tags = [
['e', channelID, '', 'root'], ["e", channelID, "", "root"],
['e', channelReply.id, '', 'reply'], ["e", channelReply.id, "", "reply"],
['p', channelReply.pubkey, ''], ["p", channelReply.pubkey, ""],
]; ];
} else { } else {
tags = [['e', channelID, '', 'root']]; tags = [["e", channelID, "", "root"]];
} }
if (!isError && !isLoading && account) { if (!isError && !isLoading && account) {
@ -57,12 +59,12 @@ export default function ChannelMessageForm({ channelID }: { channelID: string |
// reset channel reply // reset channel reply
resetChannelReply(); resetChannelReply();
} else { } else {
console.log('error'); console.log("error");
} }
}; };
const handleEnterPress = (e) => { const handleEnterPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
submitEvent(); submitEvent();
} }
@ -75,7 +77,7 @@ export default function ChannelMessageForm({ channelID }: { channelID: string |
return ( return (
<div <div
className={`relative ${ className={`relative ${
channelReply.id ? 'h-36' : 'h-24' 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`} } 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 && ( {channelReply.id && (
@ -84,10 +86,13 @@ export default function ChannelMessageForm({ channelID }: { channelID: string |
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<UserReply pubkey={channelReply.pubkey} /> <UserReply pubkey={channelReply.pubkey} />
<div className="-mt-3.5 pl-[32px]"> <div className="-mt-3.5 pl-[32px]">
<div className="text-xs text-zinc-200">{channelReply.content}</div> <div className="text-xs text-zinc-200">
{channelReply.content}
</div>
</div> </div>
</div> </div>
<button <button
type="button"
onClick={() => stopReply()} onClick={() => stopReply()}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
> >
@ -103,17 +108,18 @@ export default function ChannelMessageForm({ channelID }: { channelID: string |
spellCheck={false} spellCheck={false}
placeholder="Message" placeholder="Message"
className={`relative ${ className={`relative ${
channelReply.id ? 'h-36 pt-16' : 'h-24 pt-3' 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`} } 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="absolute bottom-2 w-full px-2">
<div className="flex w-full items-center justify-between bg-zinc-800"> <div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700"> <div className="flex items-center gap-2 divide-x divide-zinc-700">
<ImagePicker type="channel" /> <ImagePicker type="channel" />
<div className="flex items-center gap-2 pl-2"></div> <div className="flex items-center gap-2 pl-2" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button"
onClick={() => submitEvent()} onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false} 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" 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"

View File

@ -1,19 +1,19 @@
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from '@shared/tooltip'; import { Tooltip } from "@shared/tooltip";
import CancelIcon from '@icons/cancel'; import CancelIcon from "@icons/cancel";
import HideIcon from '@icons/hide'; import HideIcon from "@icons/hide";
import { channelMessagesAtom } from '@stores/channel'; import { channelMessagesAtom } from "@stores/channel";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from 'jotai'; import { useAtom } from "jotai";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { Fragment, useContext, useState } from 'react'; import { Fragment, useContext, useState } from "react";
export default function MessageHideButton({ id }: { id: string }) { export default function MessageHideButton({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -33,11 +33,11 @@ export default function MessageHideButton({ id }: { id: string }) {
const hideMessage = () => { const hideMessage = () => {
if (!isError && !isLoading && account) { if (!isError && !isLoading && account) {
const event: any = { const event: any = {
content: '', content: "",
created_at: dateToUnix(), created_at: dateToUnix(),
kind: 43, kind: 43,
pubkey: account.pubkey, pubkey: account.pubkey,
tags: [['e', id]], tags: [["e", id]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = signEvent(event, account.privkey);
@ -46,13 +46,15 @@ export default function MessageHideButton({ id }: { id: string }) {
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
// update local state // update local state
const cloneMessages = [...messages]; const cloneMessages = [...messages];
const targetMessage = cloneMessages.findIndex((message) => message.id === id); const targetMessage = cloneMessages.findIndex(
cloneMessages[targetMessage]['hide'] = true; (message) => message.id === id,
);
cloneMessages[targetMessage]["hide"] = true;
setMessages(cloneMessages); setMessages(cloneMessages);
// close modal // close modal
closeModal(); closeModal();
} else { } else {
console.log('error'); console.log("error");
} }
}; };
@ -60,6 +62,7 @@ export default function MessageHideButton({ id }: { id: string }) {
<> <>
<Tooltip message="Hide this message"> <Tooltip message="Hide this message">
<button <button
type="button"
onClick={openModal} onClick={openModal}
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800" className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
> >
@ -102,10 +105,13 @@ export default function MessageHideButton({ id }: { id: string }) {
<button <button
type="button" type="button"
onClick={closeModal} onClick={closeModal}
autoFocus={false}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon width={20} height={20} className="text-zinc-300" /> <CancelIcon
width={20}
height={20}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="leading-tight text-zinc-400"> <Dialog.Description className="leading-tight text-zinc-400">

View File

@ -1,11 +1,11 @@
import MessageHideButton from '@app/channel/components/messages/hideButton'; import MessageHideButton from "@app/channel/components/messages/hideButton";
import MessageMuteButton from '@app/channel/components/messages/muteButton'; import MessageMuteButton from "@app/channel/components/messages/muteButton";
import MessageReplyButton from '@app/channel/components/messages/replyButton'; import MessageReplyButton from "@app/channel/components/messages/replyButton";
import ChannelMessageUser from '@app/channel/components/messages/user'; import ChannelMessageUser from "@app/channel/components/messages/user";
import { noteParser } from '@utils/parser'; import { noteParser } from "@utils/parser";
import { useMemo } from 'react'; import { useMemo } from "react";
export default function ChannelMessageItem({ data }: { data: any }) { export default function ChannelMessageItem({ data }: { data: any }) {
const content = useMemo(() => noteParser(data), [data]); const content = useMemo(() => noteParser(data), [data]);
@ -17,14 +17,22 @@ export default function ChannelMessageItem({ data }: { data: any }) {
<div className="-mt-[17px] pl-[48px]"> <div className="-mt-[17px] pl-[48px]">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="whitespace-pre-line break-words text-sm leading-tight"> <div className="whitespace-pre-line break-words text-sm leading-tight">
{data.hide ? <span className="italic text-zinc-400">[hided message]</span> : content.parsed} {data.hide ? (
<span className="italic text-zinc-400">[hided message]</span>
) : (
content.parsed
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex"> <div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
<div className="inline-flex h-8 items-center justify-center gap-1.5 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800"> <div className="inline-flex h-8 items-center justify-center gap-1.5 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} /> <MessageReplyButton
id={data.id}
pubkey={data.pubkey}
content={data.content}
/>
<MessageHideButton id={data.id} /> <MessageHideButton id={data.id} />
<MessageMuteButton pubkey={data.pubkey} /> <MessageMuteButton pubkey={data.pubkey} />
</div> </div>

View File

@ -1,19 +1,19 @@
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from '@shared/tooltip'; import { Tooltip } from "@shared/tooltip";
import CancelIcon from '@icons/cancel'; import CancelIcon from "@icons/cancel";
import MuteIcon from '@icons/mute'; import MuteIcon from "@icons/mute";
import { channelMessagesAtom } from '@stores/channel'; import { channelMessagesAtom } from "@stores/channel";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from 'jotai'; import { useAtom } from "jotai";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { Fragment, useContext, useState } from 'react'; import { Fragment, useContext, useState } from "react";
export default function MessageMuteButton({ pubkey }: { pubkey: string }) { export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -33,11 +33,11 @@ export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
const muteUser = () => { const muteUser = () => {
if (!isError && !isLoading && account) { if (!isError && !isLoading && account) {
const event: any = { const event: any = {
content: '', content: "",
created_at: dateToUnix(), created_at: dateToUnix(),
kind: 44, kind: 44,
pubkey: account.pubkey, pubkey: account.pubkey,
tags: [['p', pubkey]], tags: [["p", pubkey]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = signEvent(event, account.privkey);
@ -46,12 +46,14 @@ export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
// update local state // update local state
const cloneMessages = [...messages]; const cloneMessages = [...messages];
const finalMessages = cloneMessages.filter((message) => message.pubkey !== pubkey); const finalMessages = cloneMessages.filter(
(message) => message.pubkey !== pubkey,
);
setMessages(finalMessages); setMessages(finalMessages);
// close modal // close modal
closeModal(); closeModal();
} else { } else {
console.log('error'); console.log("error");
} }
}; };
@ -59,6 +61,7 @@ export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
<> <>
<Tooltip message="Mute this user"> <Tooltip message="Mute this user">
<button <button
type="button"
onClick={() => openModal()} onClick={() => openModal()}
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800" className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
> >
@ -101,10 +104,13 @@ export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
<button <button
type="button" type="button"
onClick={closeModal} onClick={closeModal}
autoFocus={false}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon width={20} height={20} className="text-zinc-300" /> <CancelIcon
width={20}
height={20}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="leading-tight text-zinc-400"> <Dialog.Description className="leading-tight text-zinc-400">

View File

@ -1,12 +1,16 @@
import { Tooltip } from '@shared/tooltip'; import { Tooltip } from "@shared/tooltip";
import ReplyMessageIcon from '@icons/replyMessage'; import ReplyMessageIcon from "@icons/replyMessage";
import { channelReplyAtom } from '@stores/channel'; import { channelReplyAtom } from "@stores/channel";
import { useSetAtom } from 'jotai'; import { useSetAtom } from "jotai";
export default function MessageReplyButton({ 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 setChannelReplyAtom = useSetAtom(channelReplyAtom);
const createReply = () => { const createReply = () => {
@ -16,6 +20,7 @@ export default function MessageReplyButton({ id, pubkey, content }: { id: string
return ( return (
<Tooltip message="Reply to message"> <Tooltip message="Reply to message">
<button <button
type="button"
onClick={() => createReply()} onClick={() => createReply()}
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800" className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
> >

View File

@ -1,26 +1,29 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants'; import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import dayjs from 'dayjs'; import dayjs from "dayjs";
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export default function ChannelMessageUser({ pubkey, time }: { pubkey: string; time: number }) { export default function ChannelMessageUser({
pubkey,
time,
}: { pubkey: string; time: number }) {
const { user, isError, isLoading } = useProfile(pubkey); const { user, isError, isLoading } = useProfile(pubkey);
return ( return (
<div className="group flex items-start gap-3"> <div className="group flex items-start gap-3">
{isError || isLoading ? ( {isError || isLoading ? (
<> <>
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div> <div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800" />
<div className="flex w-full flex-1 items-start justify-between"> <div className="flex w-full flex-1 items-start justify-between">
<div className="flex items-baseline gap-2 text-sm"> <div className="flex items-baseline gap-2 text-sm">
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div> <div className="h-4 w-20 animate-pulse rounded bg-zinc-800" />
</div> </div>
</div> </div>
</> </>
@ -28,7 +31,9 @@ export default function ChannelMessageUser({ pubkey, time }: { pubkey: string; t
<> <>
<div className="relative h-9 w-9 shrink rounded-md"> <div className="relative h-9 w-9 shrink rounded-md">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${
user?.picture ? user.picture : DEFAULT_AVATAR
}`}
alt={pubkey} alt={pubkey}
className="h-9 w-9 rounded-md object-cover" className="h-9 w-9 rounded-md object-cover"
/> />
@ -39,7 +44,9 @@ export default function ChannelMessageUser({ pubkey, time }: { pubkey: string; t
{user?.display_name || user?.name || shortenKey(pubkey)} {user?.display_name || user?.name || shortenKey(pubkey)}
</span> </span>
<span className="leading-none text-zinc-500">·</span> <span className="leading-none text-zinc-500">·</span>
<span className="leading-none text-zinc-500">{dayjs().to(dayjs.unix(time))}</span> <span className="leading-none text-zinc-500">
{dayjs().to(dayjs.unix(time))}
</span>
</div> </div>
</div> </div>
</> </>

View File

@ -1,9 +1,9 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants'; import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
export default function UserReply({ pubkey }: { pubkey: string }) { export default function UserReply({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey); const { user, isError, isLoading } = useProfile(pubkey);
@ -12,14 +12,16 @@ export default function UserReply({ pubkey }: { pubkey: string }) {
<div className="group flex items-start gap-1"> <div className="group flex items-start gap-1">
{isError || isLoading ? ( {isError || isLoading ? (
<> <>
<div className="relative h-7 w-7 shrink animate-pulse overflow-hidden rounded bg-zinc-800"></div> <div className="relative h-7 w-7 shrink animate-pulse overflow-hidden rounded bg-zinc-800" />
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-xs font-medium leading-none text-zinc-500"></span> <span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-xs font-medium leading-none text-zinc-500" />
</> </>
) : ( ) : (
<> <>
<div className="relative h-7 w-7 shrink overflow-hidden rounded"> <div className="relative h-7 w-7 shrink overflow-hidden rounded">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${
user?.picture ? user.picture : DEFAULT_AVATAR
}`}
alt={pubkey} alt={pubkey}
className="h-7 w-7 rounded object-cover" className="h-7 w-7 rounded object-cover"
/> />

View File

@ -1,19 +1,22 @@
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile'; import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import CopyIcon from '@icons/copy'; import CopyIcon from "@icons/copy";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
import { nip19 } from 'nostr-tools'; import { nip19 } from "nostr-tools";
export default function ChannelMetadata({ id, pubkey }: { id: string; pubkey: string }) { export default function ChannelMetadata({
id,
pubkey,
}: { id: string; pubkey: string }) {
const metadata = useChannelProfile(id, pubkey); const metadata = useChannelProfile(id, pubkey);
const noteID = id ? nip19.noteEncode(id) : null; const noteID = id ? nip19.noteEncode(id) : null;
const copyNoteID = async () => { const copyNoteID = async () => {
const { writeText } = await import('@tauri-apps/api/clipboard'); const { writeText } = await import("@tauri-apps/api/clipboard");
if (noteID) { if (noteID) {
await writeText(noteID); await writeText(noteID);
} }
@ -30,13 +33,15 @@ export default function ChannelMetadata({ id, pubkey }: { id: string; pubkey: st
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<h5 className="truncate text-sm font-medium leading-none text-zinc-100">{metadata?.name}</h5> <h5 className="truncate text-sm font-medium leading-none text-zinc-100">
<button onClick={() => copyNoteID()}> {metadata?.name}
</h5>
<button type="button" onClick={() => copyNoteID()}>
<CopyIcon width={14} height={14} className="text-zinc-400" /> <CopyIcon width={14} height={14} className="text-zinc-400" />
</button> </button>
</div> </div>
<p className="text-xs leading-none text-zinc-400"> <p className="text-xs leading-none text-zinc-400">
{metadata?.about || (noteID && noteID.substring(0, 24) + '...')} {metadata?.about || (noteID && `${noteID.substring(0, 24)}...`)}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
export default function MiniMember({ pubkey }: { pubkey: string }) { export default function MiniMember({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey); const { user, isError, isLoading } = useProfile(pubkey);
@ -10,12 +10,12 @@ export default function MiniMember({ pubkey }: { pubkey: string }) {
return ( return (
<> <>
{isError || isLoading ? ( {isError || isLoading ? (
<div className="h-8 w-8 animate-pulse rounded-md bg-zinc-800"></div> <div className="h-8 w-8 animate-pulse rounded-md bg-zinc-800" />
) : ( ) : (
<Image <Image
className="inline-block h-8 w-8 rounded-md bg-white ring-2 ring-zinc-950 transition-all duration-150 ease-in-out" className="inline-block h-8 w-8 rounded-md bg-white ring-2 ring-zinc-950 transition-all duration-150 ease-in-out"
src={user?.picture || DEFAULT_AVATAR} src={user?.picture || DEFAULT_AVATAR}
alt={user?.pubkey || 'user avatar'} alt={user?.pubkey || "user avatar"}
/> />
)} )}
</> </>

View File

@ -1,18 +1,18 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import { useState } from 'react'; import { useState } from "react";
export default function MutedItem({ data }: { data: any }) { export default function MutedItem({ data }: { data: any }) {
const { user, isError, isLoading } = useProfile(data.content); const { user, isError, isLoading } = useProfile(data.content);
const [status, setStatus] = useState(data.status); const [status, setStatus] = useState(data.status);
const unmute = async () => { const unmute = async () => {
const { updateItemInBlacklist } = await import('@utils/storage'); const { updateItemInBlacklist } = await import("@utils/storage");
const res = await updateItemInBlacklist(data.content, 0); const res = await updateItemInBlacklist(data.content, 0);
if (res) { if (res) {
setStatus(0); setStatus(0);
@ -20,7 +20,7 @@ export default function MutedItem({ data }: { data: any }) {
}; };
const mute = async () => { const mute = async () => {
const { updateItemInBlacklist } = await import('@utils/storage'); const { updateItemInBlacklist } = await import("@utils/storage");
const res = await updateItemInBlacklist(data.content, 1); const res = await updateItemInBlacklist(data.content, 1);
if (res) { if (res) {
setStatus(1); setStatus(1);
@ -32,10 +32,10 @@ export default function MutedItem({ data }: { data: any }) {
{isError || isLoading ? ( {isError || isLoading ? (
<> <>
<div className="flex items-center gap-1.5"> <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="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800" />
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start"> <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-3 w-16 animate-pulse bg-zinc-800" />
<div className="h-2 w-10 animate-pulse bg-zinc-800"></div> <div className="h-2 w-10 animate-pulse bg-zinc-800" />
</div> </div>
</div> </div>
</> </>
@ -51,14 +51,17 @@ export default function MutedItem({ data }: { data: any }) {
</div> </div>
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start"> <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"> <span className="truncate text-sm font-medium leading-none text-zinc-200">
{user?.display_name || user?.name || 'Pleb'} {user?.display_name || user?.name || "Pleb"}
</span>
<span className="text-xs leading-none text-zinc-400">
{shortenKey(data.content)}
</span> </span>
<span className="text-xs leading-none text-zinc-400">{shortenKey(data.content)}</span>
</div> </div>
</div> </div>
<div> <div>
{status === 1 ? ( {status === 1 ? (
<button <button
type="button"
onClick={() => unmute()} 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" 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"
> >
@ -66,6 +69,7 @@ export default function MutedItem({ data }: { data: any }) {
</button> </button>
) : ( ) : (
<button <button
type="button"
onClick={() => mute()} 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" 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"
> >

View File

@ -1,20 +1,20 @@
import { AvatarUploader } from '@shared/avatarUploader'; import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import CancelIcon from '@icons/cancel'; import CancelIcon from "@icons/cancel";
import EditIcon from '@icons/edit'; import EditIcon from "@icons/edit";
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from '@stores/constants'; import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getChannel } from '@utils/storage'; import { getChannel } from "@utils/storage";
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from 'react'; import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from 'react-hook-form'; import { useForm } from "react-hook-form";
export default function ChannelUpdateModal({ id }: { id: string }) { export default function ChannelUpdateModal({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -58,7 +58,7 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
created_at: dateToUnix(), created_at: dateToUnix(),
kind: 41, kind: 41,
pubkey: account.pubkey, pubkey: account.pubkey,
tags: [['e', id]], tags: [["e", id]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = signEvent(event, account.privkey);
@ -71,12 +71,12 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
setIsOpen(false); setIsOpen(false);
setLoading(false); setLoading(false);
} else { } else {
console.log('error'); console.log("error");
} }
}; };
useEffect(() => { useEffect(() => {
setValue('picture', image); setValue("picture", image);
}, [setValue, image]); }, [setValue, image]);
return ( return (
@ -86,7 +86,11 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
onClick={() => openModal()} onClick={() => openModal()}
className="group inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 hover:bg-zinc-800 focus:outline-none" className="group inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 hover:bg-zinc-800 focus:outline-none"
> >
<EditIcon width={16} height={16} className="text-zinc-400 group-hover:text-zinc-200" /> <EditIcon
width={16}
height={16}
className="text-zinc-400 group-hover:text-zinc-200"
/>
</button> </button>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}> <Dialog as="div" className="relative z-10" onClose={closeModal}>
@ -124,30 +128,42 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
<button <button
type="button" type="button"
onClick={closeModal} onClick={closeModal}
autoFocus={false}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon width={20} height={20} className="text-zinc-300" /> <CancelIcon
width={20}
height={20}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="leading-tight text-zinc-400"> <Dialog.Description className="leading-tight text-zinc-400">
New metadata will be published on all relays, and will be immediately available to all users, so New metadata will be published on all relays, and will be
please carefully. immediately available to all users, so please carefully.
</Dialog.Description> </Dialog.Description>
</div> </div>
</div> </div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3"> <div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form onSubmit={handleSubmit(onSubmit)} className="flex h-full w-full flex-col gap-4"> <form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4"
>
<input <input
type={'hidden'} type={"hidden"}
{...register('picture')} {...register("picture")}
value={image} value={image}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">Picture</label> <label className="text-xs font-semibold uppercase tracking-wider text-zinc-400">
Picture
</label>
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950"> <div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
<Image src={image} alt="channel picture" className="relative z-10 h-11 w-11 rounded-md" /> <Image
src={image}
alt="channel picture"
className="relative z-10 h-11 w-11 rounded-md"
/>
<div className="absolute bottom-3 right-3 z-10"> <div className="absolute bottom-3 right-3 z-10">
<AvatarUploader valueState={setImage} /> <AvatarUploader valueState={setImage} />
</div> </div>
@ -159,8 +175,11 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
</label> </label>
<div className="relative w-full shrink-0 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"> <div className="relative w-full shrink-0 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">
<input <input
type={'text'} type={"text"}
{...register('name', { required: true, minLength: 4 })} {...register("name", {
required: true,
minLength: 4,
})}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
@ -172,7 +191,7 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
</label> </label>
<div className="relative h-20 w-full shrink-0 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"> <div className="relative h-20 w-full shrink-0 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">
<textarea <textarea
{...register('about')} {...register("about")}
spellCheck={false} spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
@ -181,7 +200,9 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
<div className="flex h-14 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2"> <div className="flex h-14 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<div className="inline-flex items-center gap-1"> <div className="inline-flex items-center gap-1">
<span className="text-sm font-bold leading-none text-zinc-200">Make Private</span> <span className="text-sm font-bold leading-none text-zinc-200">
Make Private
</span>
<div className="inline-flex items-center rounded-md bg-zinc-400/10 px-2 py-0.5 text-xs font-medium ring-1 ring-inset ring-zinc-400/20"> <div className="inline-flex items-center rounded-md bg-zinc-400/10 px-2 py-0.5 text-xs font-medium ring-1 ring-inset ring-zinc-400/20">
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
Coming soon Coming soon
@ -194,12 +215,13 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
</div> </div>
<div> <div>
<button <button
type="button"
disabled disabled
className="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent bg-zinc-900 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-600 focus:ring-offset-2" className="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent bg-zinc-900 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-600 focus:ring-offset-2"
role="switch" role="switch"
aria-checked="false" aria-checked="false"
> >
<span className="pointer-events-none inline-block h-5 w-5 translate-x-0 transform rounded-full bg-zinc-600 shadow ring-0 transition duration-200 ease-in-out"></span> <span className="pointer-events-none inline-block h-5 w-5 translate-x-0 transform rounded-full bg-zinc-600 shadow ring-0 transition duration-200 ease-in-out" />
</button> </button>
</div> </div>
</div> </div>
@ -216,6 +238,7 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<title id="loading">Loading</title>
<circle <circle
className="opacity-25" className="opacity-25"
cx="12" cx="12"
@ -223,15 +246,15 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
r="10" r="10"
stroke="currentColor" stroke="currentColor"
strokeWidth="4" strokeWidth="4"
></circle> />
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
'Update channel' "Update channel"
)} )}
</button> </button>
</div> </div>

View File

@ -1,12 +1,12 @@
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { getChannel, updateChannelMetadata } from '@utils/storage'; import { getChannel, updateChannelMetadata } from "@utils/storage";
import { useContext } from 'react'; import { useContext } from "react";
import useSWR, { useSWRConfig } from 'swr'; import useSWR, { useSWRConfig } from "swr";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
const fetcher = async ([, id]) => { const fetcher = async ([, id]) => {
const result = await getChannel(id); const result = await getChannel(id);
@ -21,14 +21,16 @@ export function useChannelProfile(id: string, channelPubkey: string) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const { data, isLoading } = useSWR(['channel-metadata', id], fetcher); const { data, isLoading } = useSWR(["channel-metadata", id], fetcher);
useSWRSubscription(!isLoading && data ? ['channel-metadata', id] : null, ([, key], {}) => { useSWRSubscription(
!isLoading && data ? ["channel-metadata", id] : null,
([, key]) => {
// subscribe to channel // subscribe to channel
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
'#e': [key], "#e": [key],
authors: [channelPubkey], authors: [channelPubkey],
kinds: [41], kinds: [41],
}, },
@ -38,19 +40,20 @@ export function useChannelProfile(id: string, channelPubkey: string) {
// update in local database // update in local database
updateChannelMetadata(key, event.content); updateChannelMetadata(key, event.content);
// revaildate // revaildate
mutate(['channel-metadata', key]); mutate(["channel-metadata", key]);
}, },
undefined, undefined,
undefined, undefined,
{ {
unsubscribeOnEose: true, unsubscribeOnEose: true,
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
return data; return data;
} }

View File

@ -1,6 +1,6 @@
import AppHeader from '@shared/appHeader'; import AppHeader from "@shared/appHeader";
import MultiAccounts from '@shared/multiAccounts'; import MultiAccounts from "@shared/multiAccounts";
import Navigation from '@shared/navigation'; import Navigation from "@shared/navigation";
export function LayoutChannel({ children }: { children: React.ReactNode }) { export function LayoutChannel({ children }: { children: React.ReactNode }) {
return ( return (
@ -20,7 +20,9 @@ export function LayoutChannel({ children }: { children: React.ReactNode }) {
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900"> <div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
<Navigation /> <Navigation />
</div> </div>
<div className="col-span-3 m-3 overflow-hidden xl:col-span-4">{children}</div> <div className="col-span-3 m-3 overflow-hidden xl:col-span-4">
{children}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,25 +1,25 @@
import ChannelBlackList from '@app/channel/components/blacklist'; import ChannelBlackList from "@app/channel/components/blacklist";
import ChannelMembers from '@app/channel/components/members'; import ChannelMembers from "@app/channel/components/members";
import ChannelMessageForm from '@app/channel/components/messages/form'; import ChannelMessageForm from "@app/channel/components/messages/form";
import ChannelMetadata from '@app/channel/components/metadata'; import ChannelMetadata from "@app/channel/components/metadata";
import ChannelUpdateModal from '@app/channel/components/updateModal'; import ChannelUpdateModal from "@app/channel/components/updateModal";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { channelMessagesAtom, channelReplyAtom } from '@stores/channel'; import { channelMessagesAtom, channelReplyAtom } from "@stores/channel";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from '@utils/date'; import { dateToUnix, getHourAgo } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { usePageContext } from '@utils/hooks/usePageContext'; import { usePageContext } from "@utils/hooks/usePageContext";
import { getActiveBlacklist, getBlacklist } from '@utils/storage'; import { getActiveBlacklist, getBlacklist } from "@utils/storage";
import { arrayObjToPureArr } from '@utils/transform'; import { arrayObjToPureArr } from "@utils/transform";
import { useSetAtom } from 'jotai'; import { useSetAtom } from "jotai";
import { useResetAtom } from 'jotai/utils'; import { useResetAtom } from "jotai/utils";
import { Suspense, lazy, useContext, useEffect, useRef } from 'react'; import { Suspense, lazy, useContext, useEffect, useRef } from "react";
import useSWR from 'swr'; import useSWR from "swr";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
const fetchMuted = async ([, id]) => { const fetchMuted = async ([, id]) => {
const res = await getBlacklist(id, 44); const res = await getBlacklist(id, 44);
@ -33,7 +33,9 @@ const fetchHided = async ([, id]) => {
return array; return array;
}; };
const ChannelMessageList = lazy(() => import('@app/channel/components/messageList')); const ChannelMessageList = lazy(
() => import("@app/channel/components/messageList"),
);
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -44,8 +46,14 @@ export function Page() {
const channelPubkey = searchParams.channelpub; const channelPubkey = searchParams.channelpub;
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
const { data: muted } = useSWR(!isLoading && !isError && account ? ['muted', account.id] : null, fetchMuted); const { data: muted } = useSWR(
const { data: hided } = useSWR(!isLoading && !isError && account ? ['hided', account.id] : null, fetchHided); !isLoading && !isError && account ? ["muted", account.id] : null,
fetchMuted,
);
const { data: hided } = useSWR(
!isLoading && !isError && account ? ["hided", account.id] : null,
fetchHided,
);
const setChannelMessages = useSetAtom(channelMessagesAtom); const setChannelMessages = useSetAtom(channelMessagesAtom);
const resetChannelMessages = useResetAtom(channelMessagesAtom); const resetChannelMessages = useResetAtom(channelMessagesAtom);
@ -53,12 +61,14 @@ export function Page() {
const now = useRef(new Date()); const now = useRef(new Date());
useSWRSubscription(account && channelID && muted && hided ? ['channel', channelID] : null, ([, key], {}: any) => { useSWRSubscription(
account && channelID && muted && hided ? ["channel", channelID] : null,
([, key]) => {
// subscribe to channel // subscribe to channel
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
'#e': [key], "#e": [key],
kinds: [42], kinds: [42],
since: dateToUnix(getHourAgo(24, now.current)), since: dateToUnix(getHourAgo(24, now.current)),
limit: 20, limit: 20,
@ -68,20 +78,21 @@ export function Page() {
(event: { id: string; pubkey: string }) => { (event: { id: string; pubkey: string }) => {
const message: any = event; const message: any = event;
if (hided.includes(event.id)) { if (hided.includes(event.id)) {
message['hide'] = true; message["hide"] = true;
} else { } else {
message['hide'] = false; message["hide"] = false;
} }
if (!muted.array.includes(event.pubkey)) { if (!muted.array.includes(event.pubkey)) {
setChannelMessages((prev) => [...prev, message]); setChannelMessages((prev) => [...prev, message]);
} }
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
useEffect(() => { useEffect(() => {
let ignore = false; let ignore = false;
@ -108,7 +119,9 @@ export function Page() {
<ChannelMembers /> <ChannelMembers />
{!muted ? <></> : <ChannelBlackList blacklist={muted.original} />} {!muted ? <></> : <ChannelBlackList blacklist={muted.original} />}
{!isLoading && !isError && account ? ( {!isLoading && !isError && account ? (
account.pubkey === channelPubkey && <ChannelUpdateModal id={channelID} /> account.pubkey === channelPubkey && (
<ChannelUpdateModal id={channelID} />
)
) : ( ) : (
<></> <></>
)} )}

View File

@ -1 +1 @@
export { LayoutChat as Layout } from './layout'; export { LayoutChat as Layout } from "./layout";

View File

@ -1,12 +1,12 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
import { usePageContext } from '@utils/hooks/usePageContext'; import { usePageContext } from "@utils/hooks/usePageContext";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export default function ChatsListItem({ pubkey }: { pubkey: string }) { export default function ChatsListItem({ pubkey }: { pubkey: string }) {
const pageContext = usePageContext(); const pageContext = usePageContext();
@ -21,17 +21,19 @@ export default function ChatsListItem({ pubkey }: { pubkey: string }) {
{isError && <div>error</div>} {isError && <div>error</div>}
{isLoading && !user ? ( {isLoading && !user ? (
<div className="inline-flex h-8 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-8 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div> <div>
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-sm font-medium"></div> <div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-sm font-medium" />
</div> </div>
</div> </div>
) : ( ) : (
<a <a
href={`/app/chat?pubkey=${pubkey}`} href={`/app/chat?pubkey=${pubkey}`}
className={twMerge( className={twMerge(
'group inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900', "group inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900",
pagePubkey === pubkey ? 'dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800' : '' pagePubkey === pubkey
? "dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
: "",
)} )}
> >
<div className="relative h-5 w-5 shrink-0 rounded"> <div className="relative h-5 w-5 shrink-0 rounded">

View File

@ -1,16 +1,19 @@
import ChatsListItem from '@app/chat/components/item'; import ChatsListItem from "@app/chat/components/item";
import ChatsListSelfItem from '@app/chat/components/self'; import ChatsListSelfItem from "@app/chat/components/self";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getChats } from '@utils/storage'; import { getChats } from "@utils/storage";
import useSWR from 'swr'; import useSWR from "swr";
const fetcher = ([, account]) => getChats(account); const fetcher = ([, account]) => getChats(account);
export default function ChatsList() { export default function ChatsList() {
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
const { data: chats, error }: any = useSWR(!isLoading && !isError && account ? ['chats', account] : null, fetcher); const { data: chats, error }: any = useSWR(
!isLoading && !isError && account ? ["chats", account] : null,
fetcher,
);
return ( return (
<div className="flex flex-col gap-px"> <div className="flex flex-col gap-px">
@ -18,16 +21,18 @@ export default function ChatsList() {
{!chats || error ? ( {!chats || error ? (
<> <>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse bg-zinc-800"></div> <div className="h-3 w-full animate-pulse bg-zinc-800" />
</div> </div>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse bg-zinc-800"></div> <div className="h-3 w-full animate-pulse bg-zinc-800" />
</div> </div>
</> </>
) : ( ) : (
chats.map((item: { pubkey: string }) => <ChatsListItem key={item.pubkey} pubkey={item.pubkey} />) chats.map((item: { pubkey: string }) => (
<ChatsListItem key={item.pubkey} pubkey={item.pubkey} />
))
)} )}
</div> </div>
); );

View File

@ -1,12 +1,12 @@
import { ChatMessageItem } from '@app/chat/components/messages/item'; import { ChatMessageItem } from "@app/chat/components/messages/item";
import { sortedChatMessagesAtom } from '@stores/chat'; import { sortedChatMessagesAtom } from "@stores/chat";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from "react";
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from "react-virtuoso";
export default function ChatMessageList() { export default function ChatMessageList() {
const { account } = useActiveAccount(); const { account } = useActiveAccount();
@ -16,16 +16,22 @@ export default function ChatMessageList() {
const itemContent: any = useCallback( const itemContent: any = useCallback(
(index: string | number) => { (index: string | number) => {
return <ChatMessageItem data={data[index]} userPubkey={account.pubkey} userPrivkey={account.privkey} />; return (
<ChatMessageItem
data={data[index]}
userPubkey={account.pubkey}
userPrivkey={account.privkey}
/>
);
}, },
[account.privkey, account.pubkey, data] [account.privkey, account.pubkey, data],
); );
const computeItemKey = useCallback( const computeItemKey = useCallback(
(index: string | number) => { (index: string | number) => {
return data[index].id; return data[index].id;
}, },
[data] [data],
); );
return ( return (

View File

@ -1,18 +1,20 @@
import { ImagePicker } from '@shared/form/imagePicker'; import { ImagePicker } from "@shared/form/imagePicker";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { chatContentAtom } from '@stores/chat'; import { chatContentAtom } from "@stores/chat";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtom } from 'jotai'; import { useAtom } from "jotai";
import { useResetAtom } from 'jotai/utils'; import { useResetAtom } from "jotai/utils";
import { getEventHash, nip04, signEvent } from 'nostr-tools'; import { getEventHash, nip04, signEvent } from "nostr-tools";
import { useCallback, useContext } from 'react'; import { useCallback, useContext } from "react";
export default function ChatMessageForm({ receiverPubkey }: { receiverPubkey: string }) { export default function ChatMessageForm({
receiverPubkey,
}: { receiverPubkey: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
@ -23,7 +25,7 @@ export default function ChatMessageForm({ receiverPubkey }: { receiverPubkey: st
async (privkey: string) => { async (privkey: string) => {
return await nip04.encrypt(privkey, receiverPubkey, value); return await nip04.encrypt(privkey, receiverPubkey, value);
}, },
[receiverPubkey, value] [receiverPubkey, value],
); );
const submitEvent = () => { const submitEvent = () => {
@ -35,7 +37,7 @@ export default function ChatMessageForm({ receiverPubkey }: { receiverPubkey: st
created_at: dateToUnix(), created_at: dateToUnix(),
kind: 4, kind: 4,
pubkey: account.pubkey, pubkey: account.pubkey,
tags: [['p', receiverPubkey]], tags: [["p", receiverPubkey]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = signEvent(event, account.privkey);
@ -49,7 +51,7 @@ export default function ChatMessageForm({ receiverPubkey }: { receiverPubkey: st
}; };
const handleEnterPress = (e) => { const handleEnterPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
submitEvent(); submitEvent();
} }
@ -71,10 +73,11 @@ export default function ChatMessageForm({ receiverPubkey }: { receiverPubkey: st
<div className="flex w-full items-center justify-between bg-zinc-800"> <div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700"> <div className="flex items-center gap-2 divide-x divide-zinc-700">
<ImagePicker type="chat" /> <ImagePicker type="chat" />
<div className="flex items-center gap-2 pl-2"></div> <div className="flex items-center gap-2 pl-2" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button"
onClick={() => submitEvent()} onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false} 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" 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"

View File

@ -1,11 +1,11 @@
import ChatMessageUser from '@app/chat/components/messages/user'; import ChatMessageUser from "@app/chat/components/messages/user";
import { useDecryptMessage } from '@app/chat/hooks/useDecryptMessage'; import { useDecryptMessage } from "@app/chat/hooks/useDecryptMessage";
import ImagePreview from '@app/note/components/preview/image'; import ImagePreview from "@app/note/components/preview/image";
import VideoPreview from '@app/note/components/preview/video'; import VideoPreview from "@app/note/components/preview/video";
import { noteParser } from '@utils/parser'; import { noteParser } from "@utils/parser";
import { memo } from 'react'; import { memo } from "react";
export const ChatMessageItem = memo(function MessageListItem({ export const ChatMessageItem = memo(function MessageListItem({
data, data,
@ -19,7 +19,7 @@ export const ChatMessageItem = memo(function MessageListItem({
const decryptedContent = useDecryptMessage(userPubkey, userPrivkey, data); const decryptedContent = useDecryptMessage(userPubkey, userPrivkey, data);
// if we have decrypted content, use it instead of the encrypted content // if we have decrypted content, use it instead of the encrypted content
if (decryptedContent) { if (decryptedContent) {
data['content'] = decryptedContent; data["content"] = decryptedContent;
} }
// parse the note content // parse the note content
const content = noteParser(data); const content = noteParser(data);
@ -29,9 +29,19 @@ export const ChatMessageItem = memo(function MessageListItem({
<div className="flex flex-col"> <div className="flex flex-col">
<ChatMessageUser pubkey={data.pubkey} time={data.created_at} /> <ChatMessageUser pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-[17px] pl-[48px]"> <div className="-mt-[17px] pl-[48px]">
<div className="whitespace-pre-line break-words text-sm leading-tight">{content.parsed}</div> <div className="whitespace-pre-line break-words text-sm leading-tight">
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>} {content.parsed}
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>} </div>
{Array.isArray(content.images) && content.images.length ? (
<ImagePreview urls={content.images} />
) : (
<></>
)}
{Array.isArray(content.videos) && content.videos.length ? (
<VideoPreview urls={content.videos} />
) : (
<></>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,26 +1,29 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants'; import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import dayjs from 'dayjs'; import dayjs from "dayjs";
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export default function ChatMessageUser({ pubkey, time }: { pubkey: string; time: number }) { export default function ChatMessageUser({
pubkey,
time,
}: { pubkey: string; time: number }) {
const { user, isError, isLoading } = useProfile(pubkey); const { user, isError, isLoading } = useProfile(pubkey);
return ( return (
<div className="group flex items-start gap-3"> <div className="group flex items-start gap-3">
{isError || isLoading ? ( {isError || isLoading ? (
<> <>
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div> <div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800" />
<div className="flex w-full flex-1 items-start justify-between"> <div className="flex w-full flex-1 items-start justify-between">
<div className="flex items-baseline gap-2 text-sm"> <div className="flex items-baseline gap-2 text-sm">
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div> <div className="h-4 w-20 animate-pulse rounded bg-zinc-800" />
</div> </div>
</div> </div>
</> </>
@ -28,7 +31,9 @@ export default function ChatMessageUser({ pubkey, time }: { pubkey: string; time
<> <>
<div className="relative h-9 w-9 shrink rounded-md"> <div className="relative h-9 w-9 shrink rounded-md">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${
user?.picture ? user.picture : DEFAULT_AVATAR
}`}
alt={pubkey} alt={pubkey}
className="h-9 w-9 rounded-md object-cover" className="h-9 w-9 rounded-md object-cover"
/> />
@ -39,7 +44,9 @@ export default function ChatMessageUser({ pubkey, time }: { pubkey: string; time
{user?.display_name || user?.name || shortenKey(pubkey)} {user?.display_name || user?.name || shortenKey(pubkey)}
</span> </span>
<span className="leading-none text-zinc-500">·</span> <span className="leading-none text-zinc-500">·</span>
<span className="leading-none text-zinc-500">{dayjs().to(dayjs.unix(time))}</span> <span className="leading-none text-zinc-500">
{dayjs().to(dayjs.unix(time))}
</span>
</div> </div>
</div> </div>
</> </>

View File

@ -1,12 +1,12 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { usePageContext } from '@utils/hooks/usePageContext'; import { usePageContext } from "@utils/hooks/usePageContext";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export default function ChatsListSelfItem() { export default function ChatsListSelfItem() {
const pageContext = usePageContext(); const pageContext = usePageContext();
@ -22,17 +22,19 @@ export default function ChatsListSelfItem() {
{isError && <div>error</div>} {isError && <div>error</div>}
{isLoading && !account ? ( {isLoading && !account ? (
<div className="inline-flex h-8 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-8 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div> <div>
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-sm font-medium"></div> <div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-sm font-medium" />
</div> </div>
</div> </div>
) : ( ) : (
<a <a
href={`/app/chat?pubkey=${account.pubkey}`} href={`/app/chat?pubkey=${account.pubkey}`}
className={twMerge( className={twMerge(
'inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900', "inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900",
pagePubkey === account.pubkey ? 'dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800' : '' pagePubkey === account.pubkey
? "dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
: "",
)} )}
> >
<div className="relative h-5 w-5 shrink-0 rounded"> <div className="relative h-5 w-5 shrink-0 rounded">
@ -44,7 +46,9 @@ export default function ChatsListSelfItem() {
</div> </div>
<div> <div>
<h5 className="truncate text-[13px] font-semibold text-zinc-400"> <h5 className="truncate text-[13px] font-semibold text-zinc-400">
{profile?.display_name || profile?.name || shortenKey(account.pubkey)}{' '} {profile?.display_name ||
profile?.name ||
shortenKey(account.pubkey)}{" "}
<span className="text-zinc-500">(you)</span> <span className="text-zinc-500">(you)</span>
</h5> </h5>
</div> </div>

View File

@ -1,11 +1,15 @@
import { nip04 } from 'nostr-tools'; import { nip04 } from "nostr-tools";
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from "react";
export function useDecryptMessage(userKey: string, userPriv: string, data: any) { export function useDecryptMessage(
userKey: string,
userPriv: string,
data: any,
) {
const [content, setContent] = useState(null); const [content, setContent] = useState(null);
const extractSenderKey = useCallback(() => { const extractSenderKey = useCallback(() => {
const keyInTags = data.tags.find(([k, v]) => k === 'p' && v && v !== '')[1]; const keyInTags = data.tags.find(([k, v]) => k === "p" && v && v !== "")[1];
if (keyInTags === userKey) { if (keyInTags === userKey) {
return data.pubkey; return data.pubkey;
} else { } else {

View File

@ -1,6 +1,6 @@
import AppHeader from '@shared/appHeader'; import AppHeader from "@shared/appHeader";
import MultiAccounts from '@shared/multiAccounts'; import MultiAccounts from "@shared/multiAccounts";
import Navigation from '@shared/navigation'; import Navigation from "@shared/navigation";
export function LayoutChat({ children }: { children: React.ReactNode }) { export function LayoutChat({ children }: { children: React.ReactNode }) {
return ( return (
@ -20,7 +20,9 @@ export function LayoutChat({ children }: { children: React.ReactNode }) {
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900"> <div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
<Navigation /> <Navigation />
</div> </div>
<div className="col-span-3 m-3 overflow-hidden xl:col-span-4">{children}</div> <div className="col-span-3 m-3 overflow-hidden xl:col-span-4">
{children}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,19 +1,19 @@
import ChatMessageForm from '@app/chat/components/messages/form'; import ChatMessageForm from "@app/chat/components/messages/form";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { chatMessagesAtom } from '@stores/chat'; import { chatMessagesAtom } from "@stores/chat";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { usePageContext } from '@utils/hooks/usePageContext'; import { usePageContext } from "@utils/hooks/usePageContext";
import { useSetAtom } from 'jotai'; import { useSetAtom } from "jotai";
import { useResetAtom } from 'jotai/utils'; import { useResetAtom } from "jotai/utils";
import { Suspense, lazy, useContext, useEffect } from 'react'; import { Suspense, lazy, useContext, useEffect } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
const ChatMessageList = lazy(() => import('@app/chat/components/messageList')); const ChatMessageList = lazy(() => import("@app/chat/components/messageList"));
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -27,26 +27,26 @@ export function Page() {
const setChatMessages = useSetAtom(chatMessagesAtom); const setChatMessages = useSetAtom(chatMessagesAtom);
const resetChatMessages = useResetAtom(chatMessagesAtom); const resetChatMessages = useResetAtom(chatMessagesAtom);
useSWRSubscription(account ? ['chat', pubkey] : null, ([, key], {}: any) => { useSWRSubscription(account ? ["chat", pubkey] : null, ([, key]) => {
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
kinds: [4], kinds: [4],
authors: [key], authors: [key],
'#p': [account.pubkey], "#p": [account.pubkey],
limit: 20, limit: 20,
}, },
{ {
kinds: [4], kinds: [4],
authors: [account.pubkey], authors: [account.pubkey],
'#p': [key], "#p": [key],
limit: 20, limit: 20,
}, },
], ],
READONLY_RELAYS, READONLY_RELAYS,
(event: any) => { (event: any) => {
setChatMessages((prev) => [...prev, event]); setChatMessages((prev) => [...prev, event]);
} },
); );
return () => { return () => {

View File

@ -1 +1 @@
export const filesystemRoutingRoot = '/'; export const filesystemRoutingRoot = "/";

View File

@ -1,24 +1,26 @@
import { getActiveAccount } from '@utils/storage'; import { getActiveAccount } from "@utils/storage";
import useSWR from 'swr'; import useSWR from "swr";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
const fetcher = () => getActiveAccount(); const fetcher = () => getActiveAccount();
export function Page() { export function Page() {
const { data, isLoading } = useSWR('account', fetcher, { const { data, isLoading } = useSWR("account", fetcher, {
revalidateIfStale: false, revalidateIfStale: false,
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: false, revalidateOnReconnect: false,
}); });
if (!isLoading && !data) { if (!isLoading && !data) {
navigate('/auth', { overwriteLastHistoryEntry: true }); navigate("/auth", { overwriteLastHistoryEntry: true });
} }
if (!isLoading && data) { if (!isLoading && data) {
navigate('/app/inital-data', { overwriteLastHistoryEntry: true }); navigate("/app/inital-data", { overwriteLastHistoryEntry: true });
} }
return <div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white"></div>; return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white" />
);
} }

View File

@ -1,10 +1,10 @@
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import LumeIcon from '@icons/lume'; import LumeIcon from "@icons/lume";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from '@utils/date'; import { dateToUnix, getHourAgo } from "@utils/date";
import { import {
addToBlacklist, addToBlacklist,
countTotalLongNotes, countTotalLongNotes,
@ -14,11 +14,11 @@ import {
getActiveAccount, getActiveAccount,
getLastLogin, getLastLogin,
updateLastLogin, updateLastLogin,
} from '@utils/storage'; } from "@utils/storage";
import { getParentID, nip02ToArray } from '@utils/transform'; import { getParentID, nip02ToArray } from "@utils/transform";
import { useContext, useEffect, useRef } from 'react'; import { useContext, useEffect, useRef } from "react";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -71,7 +71,7 @@ export function Page() {
// kind 4 (chats) query // kind 4 (chats) query
query.push({ query.push({
kinds: [4], kinds: [4],
'#p': [account.pubkey], "#p": [account.pubkey],
since: 0, since: 0,
until: dateToUnix(now.current), until: dateToUnix(now.current),
}); });
@ -98,7 +98,7 @@ export function Page() {
(event: any) => { (event: any) => {
switch (event.kind) { switch (event.kind) {
// short text note // short text note
case 1: case 1: {
const parentID = getParentID(event.tags, event.id); const parentID = getParentID(event.tags, event.id);
// insert event to local database // insert event to local database
createNote( createNote(
@ -109,9 +109,10 @@ export function Page() {
event.tags, event.tags,
event.content, event.content,
event.created_at, event.created_at,
parentID parentID,
); );
break; break;
}
// chat // chat
case 4: case 4:
if (event.pubkey !== account.pubkey) { if (event.pubkey !== account.pubkey) {
@ -128,18 +129,18 @@ export function Page() {
event.tags, event.tags,
event.content, event.content,
event.created_at, event.created_at,
event.id event.id,
); );
break; break;
// hide message (channel only) // hide message (channel only)
case 43: case 43:
if (event.tags[0][0] === 'e') { if (event.tags[0][0] === "e") {
addToBlacklist(account.id, event.tags[0][1], 43, 1); addToBlacklist(account.id, event.tags[0][1], 43, 1);
} }
break; break;
// mute user (channel only) // mute user (channel only)
case 44: case 44:
if (event.tags[0][0] === 'p') { if (event.tags[0][0] === "p") {
addToBlacklist(account.id, event.tags[0][1], 44, 1); addToBlacklist(account.id, event.tags[0][1], 44, 1);
} }
break; break;
@ -152,7 +153,7 @@ export function Page() {
event.tags, event.tags,
event.content, event.content,
event.created_at, event.created_at,
'' "",
); );
break; break;
// long post // long post
@ -166,7 +167,7 @@ export function Page() {
event.tags, event.tags,
event.content, event.content,
event.created_at, event.created_at,
'' "",
); );
break; break;
default: default:
@ -177,9 +178,9 @@ export function Page() {
() => { () => {
updateLastLogin(dateToUnix(now.current)); updateLastLogin(dateToUnix(now.current));
timeout = setTimeout(() => { timeout = setTimeout(() => {
navigate('/app/today', { overwriteLastHistoryEntry: true }); navigate("/app/today", { overwriteLastHistoryEntry: true });
}, 5000); }, 5000);
} },
); );
}; };
@ -197,7 +198,10 @@ export function Page() {
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white"> <div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
<div className="relative h-full overflow-hidden"> <div className="relative h-full overflow-hidden">
{/* dragging area */} {/* dragging area */}
<div data-tauri-drag-region className="absolute left-0 top-0 z-20 h-16 w-full bg-transparent" /> <div
data-tauri-drag-region
className="absolute left-0 top-0 z-20 h-16 w-full bg-transparent"
/>
{/* end dragging area */} {/* end dragging area */}
<div className="relative flex h-full flex-col items-center justify-center"> <div className="relative flex h-full flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
@ -207,7 +211,8 @@ export function Page() {
Here&apos;s an interesting fact: Here&apos;s an interesting fact:
</h3> </h3>
<p className="font-medium text-zinc-300 dark:text-zinc-600"> <p className="font-medium text-zinc-300 dark:text-zinc-600">
Bitcoin and Nostr can be used by anyone, and no one can stop you! Bitcoin and Nostr can be used by anyone, and no one can stop
you!
</p> </p>
</div> </div>
</div> </div>
@ -218,12 +223,20 @@ export function Page() {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
</div> </div>
</div> </div>

View File

@ -1 +1 @@
export { LayoutNewsfeed as Layout } from './layout'; export { LayoutNewsfeed as Layout } from "./layout";

View File

@ -1,25 +1,28 @@
import { Kind1 } from '@app/note/components/kind1'; import { Kind1 } from "@app/note/components/kind1";
import { Kind1063 } from '@app/note/components/kind1063'; import { Kind1063 } from "@app/note/components/kind1063";
import NoteMetadata from '@app/note/components/metadata'; import NoteMetadata from "@app/note/components/metadata";
import { NoteParent } from '@app/note/components/parent'; import { NoteParent } from "@app/note/components/parent";
import { NoteDefaultUser } from '@app/note/components/user/default'; import { NoteDefaultUser } from "@app/note/components/user/default";
import { NoteWrapper } from '@app/note/components/wrapper'; import { NoteWrapper } from "@app/note/components/wrapper";
import { noteParser } from '@utils/parser'; import { noteParser } from "@utils/parser";
import { isTagsIncludeID } from '@utils/transform'; import { isTagsIncludeID } from "@utils/transform";
import { useMemo } from 'react'; import { useMemo } from "react";
export function NoteBase({ event }: { event: any }) { export function NoteBase({ event }: { event: any }) {
const content = useMemo(() => noteParser(event), [event]); const content = useMemo(() => noteParser(event), [event]);
const checkParentID = isTagsIncludeID(event.parent_id, event.tags); const checkParentID = isTagsIncludeID(event.parent_id, event.tags);
const href = event.parent_id ? `/app/note?id=${event.parent_id}` : `/app/note?id=${event.event_id}`; const href = event.parent_id
? `/app/note?id=${event.parent_id}`
: `/app/note?id=${event.event_id}`;
return ( return (
<NoteWrapper href={href} className="h-min w-full px-3 py-1.5"> <NoteWrapper href={href} className="h-min w-full px-3 py-1.5">
<div className="rounded-md border border-zinc-800 bg-zinc-900 px-3 pt-3 shadow-input shadow-black/20"> <div className="rounded-md border border-zinc-800 bg-zinc-900 px-3 pt-3 shadow-input shadow-black/20">
{event.parent_id && (event.parent_id !== event.event_id || checkParentID) ? ( {event.parent_id &&
(event.parent_id !== event.event_id || checkParentID) ? (
<NoteParent id={event.parent_id} /> <NoteParent id={event.parent_id} />
) : ( ) : (
<></> <></>

View File

@ -1,10 +1,10 @@
import { MentionNote } from '@app/note/components/mentions/note'; import { MentionNote } from "@app/note/components/mentions/note";
import { MentionUser } from '@app/note/components/mentions/user'; import { MentionUser } from "@app/note/components/mentions/user";
import ImagePreview from '@app/note/components/preview/image'; import ImagePreview from "@app/note/components/preview/image";
import VideoPreview from '@app/note/components/preview/video'; import VideoPreview from "@app/note/components/preview/video";
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from "react-markdown";
import remarkGfm from 'remark-gfm'; import remarkGfm from "remark-gfm";
export function Kind1({ content }: { content: any }) { export function Kind1({ content }: { content: any }) {
return ( return (
@ -19,10 +19,20 @@ export function Kind1({ content }: { content: any }) {
> >
{content.parsed} {content.parsed}
</ReactMarkdown> </ReactMarkdown>
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>} {Array.isArray(content.images) && content.images.length ? (
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>} <ImagePreview urls={content.images} />
) : (
<></>
)}
{Array.isArray(content.videos) && content.videos.length ? (
<VideoPreview urls={content.videos} />
) : (
<></>
)}
{Array.isArray(content.notes) && content.notes.length ? ( {Array.isArray(content.notes) && content.notes.length ? (
content.notes.map((note: string) => <MentionNote key={note} id={note} />) content.notes.map((note: string) => (
<MentionNote key={note} id={note} />
))
) : ( ) : (
<></> <></>
)} )}

View File

@ -1,4 +1,4 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
function isImage(url: string) { function isImage(url: string) {
return /\.(jpg|jpeg|gif|png|webp|avif)$/.test(url); return /\.(jpg|jpeg|gif|png|webp|avif)$/.test(url);
@ -9,7 +9,13 @@ export function Kind1063({ metadata }: { metadata: string[] }) {
return ( return (
<div className="mt-3"> <div className="mt-3">
{isImage(url) && <Image src={url} alt="image" className="h-auto w-full rounded-lg object-cover" />} {isImage(url) && (
<Image
src={url}
alt="image"
className="h-auto w-full rounded-lg object-cover"
/>
)}
</div> </div>
); );
} }

View File

@ -1,22 +1,24 @@
import { Kind1 } from '@app/note/components/kind1'; import { Kind1 } from "@app/note/components/kind1";
import { Kind1063 } from '@app/note/components/kind1063'; import { Kind1063 } from "@app/note/components/kind1063";
import { NoteSkeleton } from '@app/note/components/skeleton'; import { NoteSkeleton } from "@app/note/components/skeleton";
import { NoteDefaultUser } from '@app/note/components/user/default'; import { NoteDefaultUser } from "@app/note/components/user/default";
import { NoteWrapper } from '@app/note/components/wrapper'; import { NoteWrapper } from "@app/note/components/wrapper";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { noteParser } from '@utils/parser'; import { noteParser } from "@utils/parser";
import { memo, useContext } from 'react'; import { memo, useContext } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
export const MentionNote = memo(function MentionNote({ id }: { id: string }) { export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { data, error } = useSWRSubscription(id ? id : null, (key, { next }) => { const { data, error } = useSWRSubscription(
id ? id : null,
(key, { next }) => {
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
@ -31,19 +33,23 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
undefined, undefined,
{ {
unsubscribeOnEose: true, unsubscribeOnEose: true,
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
const kind1 = !error && data?.kind === 1 ? noteParser(data) : null; const kind1 = !error && data?.kind === 1 ? noteParser(data) : null;
const kind1063 = !error && data?.kind === 1063 ? data.tags : null; const kind1063 = !error && data?.kind === 1063 ? data.tags : null;
return ( return (
<NoteWrapper href={`/app/note?id=${id}`} className="mt-3 rounded-lg border border-zinc-800 px-3 py-3"> <NoteWrapper
href={`/app/note?id=${id}`}
className="mt-3 rounded-lg border border-zinc-800 px-3 py-3"
>
{data ? ( {data ? (
<> <>
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} /> <NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />

View File

@ -1,9 +1,13 @@
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
export function MentionUser(props: { children: any[] }) { export function MentionUser(props: { children: any[] }) {
const pubkey = props.children[0]; const pubkey = props.children[0];
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
return <span className="text-fuchsia-500">@{user?.name || user?.display_name || shortenKey(pubkey)}</span>; return (
<span className="text-fuchsia-500">
@{user?.name || user?.display_name || shortenKey(pubkey)}
</span>
);
} }

View File

@ -1,28 +1,31 @@
import NoteLike from '@app/note/components/metadata/like'; import NoteLike from "@app/note/components/metadata/like";
import NoteReply from '@app/note/components/metadata/reply'; import NoteReply from "@app/note/components/metadata/reply";
import NoteRepost from '@app/note/components/metadata/repost'; import NoteRepost from "@app/note/components/metadata/repost";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import ZapIcon from '@icons/zap'; import ZapIcon from "@icons/zap";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { useContext, useState } from 'react'; import { useContext, useState } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
export default function NoteMetadata({ id, eventPubkey }: { id: string; eventPubkey: string }) { export default function NoteMetadata({
id,
eventPubkey,
}: { id: string; eventPubkey: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const [replies, setReplies] = useState(0); const [replies, setReplies] = useState(0);
const [reposts, setReposts] = useState(0); const [reposts, setReposts] = useState(0);
const [likes, setLikes] = useState(0); const [likes, setLikes] = useState(0);
useSWRSubscription(id ? ['note-metadata', id] : null, ([, key], {}) => { useSWRSubscription(id ? ["note-metadata", id] : null, ([, key]) => {
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
'#e': [key], "#e": [key],
since: 0, since: 0,
kinds: [1, 6, 7], kinds: [1, 6, 7],
limit: 20, limit: 20,
@ -38,14 +41,14 @@ export default function NoteMetadata({ id, eventPubkey }: { id: string; eventPub
setReposts((reposts) => reposts + 1); setReposts((reposts) => reposts + 1);
break; break;
case 7: case 7:
if (event.content === '🤙' || event.content === '+') { if (event.content === "🤙" || event.content === "+") {
setLikes((likes) => likes + 1); setLikes((likes) => likes + 1);
} }
break; break;
default: default:
break; break;
} }
} },
); );
return () => { return () => {
@ -58,9 +61,18 @@ export default function NoteMetadata({ id, eventPubkey }: { id: string; eventPub
<NoteReply id={id} replies={replies} /> <NoteReply id={id} replies={replies} />
<NoteLike id={id} pubkey={eventPubkey} likes={likes} /> <NoteLike id={id} pubkey={eventPubkey} likes={likes} />
<NoteRepost id={id} pubkey={eventPubkey} reposts={reposts} /> <NoteRepost id={id} pubkey={eventPubkey} reposts={reposts} />
<button className="group inline-flex w-min items-center gap-1.5"> <button
<ZapIcon width={20} height={20} className="text-zinc-400 group-hover:text-orange-400" /> type="button"
<span className="text-sm leading-none text-zinc-400 group-hover:text-zinc-200">{0}</span> className="group inline-flex w-min items-center gap-1.5"
>
<ZapIcon
width={20}
height={20}
className="text-zinc-400 group-hover:text-orange-400"
/>
<span className="text-sm leading-none text-zinc-400 group-hover:text-zinc-200">
{0}
</span>
</button> </button>
</div> </div>
); );

View File

@ -1,16 +1,20 @@
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import LikeIcon from '@icons/like'; import LikeIcon from "@icons/like";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from "react";
export default function NoteLike({ id, pubkey, likes }: { id: string; pubkey: string; likes: number }) { export default function NoteLike({
id,
pubkey,
likes,
}: { id: string; pubkey: string; likes: number }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
@ -21,11 +25,11 @@ export default function NoteLike({ id, pubkey, likes }: { id: string; pubkey: st
if (!isLoading && !isError && account) { if (!isLoading && !isError && account) {
const event: any = { const event: any = {
content: '+', content: "+",
kind: 7, kind: 7,
tags: [ tags: [
['e', id], ["e", id],
['p', pubkey], ["p", pubkey],
], ],
created_at: dateToUnix(), created_at: dateToUnix(),
pubkey: account.pubkey, pubkey: account.pubkey,
@ -37,7 +41,7 @@ export default function NoteLike({ id, pubkey, likes }: { id: string; pubkey: st
// update state // update state
setCount(count + 1); setCount(count + 1);
} else { } else {
console.log('error'); console.log("error");
} }
}; };
@ -46,9 +50,19 @@ export default function NoteLike({ id, pubkey, likes }: { id: string; pubkey: st
}, [likes]); }, [likes]);
return ( return (
<button type="button" onClick={(e) => submitEvent(e)} className="group inline-flex w-min items-center gap-1.5"> <button
<LikeIcon width={16} height={16} className="text-zinc-400 group-hover:text-rose-400" /> type="button"
<span className="text-sm leading-none text-zinc-400 group-hover:text-zinc-200">{count}</span> onClick={(e) => submitEvent(e)}
className="group inline-flex w-min items-center gap-1.5"
>
<LikeIcon
width={16}
height={16}
className="text-zinc-400 group-hover:text-rose-400"
/>
<span className="text-sm leading-none text-zinc-400 group-hover:text-zinc-200">
{count}
</span>
</button> </button>
); );
} }

View File

@ -1,23 +1,26 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import ReplyIcon from '@icons/reply'; import ReplyIcon from "@icons/reply";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from 'react'; import { Fragment, useContext, useEffect, useState } from "react";
export default function NoteReply({ id, replies }: { id: string; replies: number }) { export default function NoteReply({
id,
replies,
}: { id: string; replies: number }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState(''); const [value, setValue] = useState("");
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
const profile = account ? JSON.parse(account.metadata) : null; const profile = account ? JSON.parse(account.metadata) : null;
@ -37,7 +40,7 @@ export default function NoteReply({ id, replies }: { id: string; replies: number
created_at: dateToUnix(), created_at: dateToUnix(),
kind: 1, kind: 1,
pubkey: account.pubkey, pubkey: account.pubkey,
tags: [['e', id]], tags: [["e", id]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = signEvent(event, account.privkey);
@ -48,7 +51,7 @@ export default function NoteReply({ id, replies }: { id: string; replies: number
setIsOpen(false); setIsOpen(false);
setCount(count + 1); setCount(count + 1);
} else { } else {
console.log('error'); console.log("error");
} }
}; };
@ -58,9 +61,19 @@ export default function NoteReply({ id, replies }: { id: string; replies: number
return ( return (
<> <>
<button type="button" onClick={() => openModal()} className="group inline-flex w-min items-center gap-1.5"> <button
<ReplyIcon width={16} height={16} className="text-zinc-400 group-hover:text-green-400" /> type="button"
<span className="text-sm leading-none text-zinc-400 group-hover:text-zinc-200">{count}</span> onClick={() => openModal()}
className="group inline-flex w-min items-center gap-1.5"
>
<ReplyIcon
width={16}
height={16}
className="text-zinc-400 group-hover:text-green-400"
/>
<span className="text-sm leading-none text-zinc-400 group-hover:text-zinc-200">
{count}
</span>
</button> </button>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}> <Dialog as="div" className="relative z-10" onClose={closeModal}>
@ -91,7 +104,11 @@ export default function NoteReply({ id, replies }: { id: string; replies: number
<div className="flex gap-2"> <div className="flex gap-2">
<div> <div>
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10"> <div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
<Image src={profile?.picture} alt="user's avatar" className="h-11 w-11 rounded-md object-cover" /> <Image
src={profile?.picture}
alt="user's avatar"
className="h-11 w-11 rounded-md object-cover"
/>
</div> </div>
</div> </div>
<div className="relative h-24 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20"> <div className="relative h-24 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
@ -107,10 +124,11 @@ export default function NoteReply({ id, replies }: { id: string; replies: number
<div className="absolute bottom-2 w-full px-2"> <div className="absolute bottom-2 w-full px-2">
<div className="flex w-full items-center justify-between bg-zinc-800"> <div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700"> <div className="flex items-center gap-2 divide-x divide-zinc-700">
<div className="flex items-center gap-2 pl-2"></div> <div className="flex items-center gap-2 pl-2" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button"
onClick={() => submitEvent()} onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false} 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-md shadow-fuchsia-900/50 hover:bg-fuchsia-600" className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-md shadow-fuchsia-900/50 hover:bg-fuchsia-600"

View File

@ -1,16 +1,20 @@
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import RepostIcon from '@icons/repost'; import RepostIcon from "@icons/repost";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from "react";
export default function NoteRepost({ id, pubkey, reposts }: { id: string; pubkey: string; reposts: number }) { export default function NoteRepost({
id,
pubkey,
reposts,
}: { id: string; pubkey: string; reposts: number }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
@ -21,11 +25,11 @@ export default function NoteRepost({ id, pubkey, reposts }: { id: string; pubkey
if (!isLoading && !isError && account) { if (!isLoading && !isError && account) {
const event: any = { const event: any = {
content: '', content: "",
kind: 6, kind: 6,
tags: [ tags: [
['e', id], ["e", id],
['p', pubkey], ["p", pubkey],
], ],
created_at: dateToUnix(), created_at: dateToUnix(),
pubkey: account.pubkey, pubkey: account.pubkey,
@ -37,7 +41,7 @@ export default function NoteRepost({ id, pubkey, reposts }: { id: string; pubkey
// update state // update state
setCount(count + 1); setCount(count + 1);
} else { } else {
console.log('error'); console.log("error");
} }
}; };
@ -46,9 +50,19 @@ export default function NoteRepost({ id, pubkey, reposts }: { id: string; pubkey
}, [reposts]); }, [reposts]);
return ( return (
<button type="button" onClick={(e) => submitEvent(e)} className="group inline-flex w-min items-center gap-1.5"> <button
<RepostIcon width={16} height={16} className="text-zinc-400 group-hover:text-blue-400" /> type="button"
<span className="text-sm leading-none text-zinc-400 group-hover:text-zinc-200">{count}</span> onClick={(e) => submitEvent(e)}
className="group inline-flex w-min items-center gap-1.5"
>
<RepostIcon
width={16}
height={16}
className="text-zinc-400 group-hover:text-blue-400"
/>
<span className="text-sm leading-none text-zinc-400 group-hover:text-zinc-200">
{count}
</span>
</button> </button>
); );
} }

View File

@ -1,22 +1,24 @@
import { Kind1 } from '@app/note/components/kind1'; import { Kind1 } from "@app/note/components/kind1";
import { Kind1063 } from '@app/note/components/kind1063'; import { Kind1063 } from "@app/note/components/kind1063";
import NoteMetadata from '@app/note/components/metadata'; import NoteMetadata from "@app/note/components/metadata";
import { NoteSkeleton } from '@app/note/components/skeleton'; import { NoteSkeleton } from "@app/note/components/skeleton";
import { NoteDefaultUser } from '@app/note/components/user/default'; import { NoteDefaultUser } from "@app/note/components/user/default";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { noteParser } from '@utils/parser'; import { noteParser } from "@utils/parser";
import { memo, useContext } from 'react'; import { memo, useContext } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
export const NoteParent = memo(function NoteParent({ id }: { id: string }) { export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { data, error } = useSWRSubscription(id ? id : null, (key, { next }) => { const { data, error } = useSWRSubscription(
id ? id : null,
(key, { next }) => {
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
@ -31,20 +33,21 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
undefined, undefined,
{ {
unsubscribeOnEose: true, unsubscribeOnEose: true,
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
const kind1 = !error && data?.kind === 1 ? noteParser(data) : null; const kind1 = !error && data?.kind === 1 ? noteParser(data) : null;
const kind1063 = !error && data?.kind === 1063 ? data.tags : null; const kind1063 = !error && data?.kind === 1063 ? data.tags : null;
return ( return (
<div className="relative flex flex-col pb-6"> <div className="relative flex flex-col pb-6">
<div className="absolute left-[16px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div> <div className="absolute left-[16px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
{data ? ( {data ? (
<> <>
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} /> <NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />

View File

@ -1,10 +1,14 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
export default function ImagePreview({ urls }: { urls: string[] }) { export default function ImagePreview({ urls }: { urls: string[] }) {
return ( return (
<div className="mt-3 grid h-full w-full grid-cols-3"> <div className="mt-3 grid h-full w-full grid-cols-3">
<div className="col-span-3"> <div className="col-span-3">
<Image src={urls[0]} alt="image" className="h-auto w-full rounded-lg object-cover" /> <Image
src={urls[0]}
alt="image"
className="h-auto w-full rounded-lg object-cover"
/>
</div> </div>
</div> </div>
); );

View File

@ -1,9 +1,9 @@
import { MediaOutlet, MediaPlayer } from '@vidstack/react'; import { MediaOutlet, MediaPlayer } from "@vidstack/react";
export default function VideoPreview({ urls }: { urls: string[] }) { export default function VideoPreview({ urls }: { urls: string[] }) {
return ( return (
<div <div
onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
className="relative mt-2 flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950" className="relative mt-2 flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950"
> >
<MediaPlayer src={urls[0]} poster="" controls> <MediaPlayer src={urls[0]} poster="" controls>

View File

@ -1,17 +1,20 @@
import { RootNote } from '@app/note/components/rootNote'; import { RootNote } from "@app/note/components/rootNote";
import { NoteRepostUser } from '@app/note/components/user/repost'; import { NoteRepostUser } from "@app/note/components/user/repost";
import { NoteWrapper } from '@app/note/components/wrapper'; import { NoteWrapper } from "@app/note/components/wrapper";
import { getQuoteID } from '@utils/transform'; import { getQuoteID } from "@utils/transform";
export function NoteQuoteRepost({ event }: { event: any }) { export function NoteQuoteRepost({ event }: { event: any }) {
const rootID = getQuoteID(event.tags); const rootID = getQuoteID(event.tags);
return ( return (
<NoteWrapper href={`/app/note?id=${rootID}`} className="h-min w-full px-3 py-1.5"> <NoteWrapper
href={`/app/note?id=${rootID}`}
className="h-min w-full px-3 py-1.5"
>
<div className="rounded-md border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20"> <div className="rounded-md border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
<div className="relative px-3 pb-5 pt-3"> <div className="relative px-3 pb-5 pt-3">
<div className="absolute left-[29px] top-[20px] h-[70px] w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div> <div className="absolute left-[29px] top-[20px] h-[70px] w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
<NoteRepostUser pubkey={event.pubkey} time={event.created_at} /> <NoteRepostUser pubkey={event.pubkey} time={event.created_at} />
</div> </div>
<RootNote id={rootID} fallback={event.content} /> <RootNote id={rootID} fallback={event.content} />

View File

@ -1,19 +1,19 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { useContext, useState } from 'react'; import { useContext, useState } from "react";
export default function NoteReplyForm({ id }: { id: string }) { export default function NoteReplyForm({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
const [value, setValue] = useState(''); const [value, setValue] = useState("");
const profile = account ? JSON.parse(account.metadata) : null; const profile = account ? JSON.parse(account.metadata) : null;
const submitEvent = () => { const submitEvent = () => {
@ -23,7 +23,7 @@ export default function NoteReplyForm({ id }: { id: string }) {
created_at: dateToUnix(), created_at: dateToUnix(),
kind: 1, kind: 1,
pubkey: account.pubkey, pubkey: account.pubkey,
tags: [['e', id]], tags: [["e", id]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = signEvent(event, account.privkey);
@ -31,9 +31,9 @@ export default function NoteReplyForm({ id }: { id: string }) {
// publish note // publish note
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
// reset form // reset form
setValue(''); setValue("");
} else { } else {
console.log('error'); console.log("error");
} }
}; };
@ -41,7 +41,11 @@ export default function NoteReplyForm({ id }: { id: string }) {
<div className="flex gap-2.5 px-3 py-4"> <div className="flex gap-2.5 px-3 py-4">
<div> <div>
<div className="relative h-9 w-9 shrink-0 overflow-hidden rounded-md"> <div className="relative h-9 w-9 shrink-0 overflow-hidden rounded-md">
<Image src={profile?.picture} alt={account?.pubkey} className="h-9 w-9 rounded-md object-cover" /> <Image
src={profile?.picture}
alt={account?.pubkey}
className="h-9 w-9 rounded-md object-cover"
/>
</div> </div>
</div> </div>
<div className="relative h-24 w-full flex-1 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"> <div className="relative h-24 w-full flex-1 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">
@ -56,9 +60,10 @@ export default function NoteReplyForm({ id }: { id: string }) {
</div> </div>
<div className="absolute bottom-2 w-full px-2"> <div className="absolute bottom-2 w-full px-2">
<div className="flex w-full items-center justify-between bg-zinc-800"> <div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700"></div> <div className="flex items-center gap-2 divide-x divide-zinc-700" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button"
onClick={() => submitEvent()} onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false} 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" 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"

View File

@ -1,7 +1,7 @@
import { Kind1 } from '@app/note/components/kind1'; import { Kind1 } from "@app/note/components/kind1";
import NoteReplyUser from '@app/note/components/user/reply'; import NoteReplyUser from "@app/note/components/user/reply";
import { noteParser } from '@utils/parser'; import { noteParser } from "@utils/parser";
export default function Reply({ data }: { data: any }) { export default function Reply({ data }: { data: any }) {
const content = noteParser(data); const content = noteParser(data);

View File

@ -1,24 +1,26 @@
import NoteReplyForm from '@app/note/components/replies/form'; import NoteReplyForm from "@app/note/components/replies/form";
import Reply from '@app/note/components/replies/item'; import Reply from "@app/note/components/replies/item";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { sortEvents } from '@utils/transform'; import { sortEvents } from "@utils/transform";
import { useContext } from 'react'; import { useContext } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
export default function RepliesList({ id }: { id: string }) { export default function RepliesList({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { data, error } = useSWRSubscription(id ? ['note-replies', id] : null, ([, key], { next }) => { const { data, error } = useSWRSubscription(
id ? ["note-replies", id] : null,
([, key], { next }) => {
// subscribe to note // subscribe to note
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
'#e': [key], "#e": [key],
since: 0, since: 0,
kinds: [1, 1063], kinds: [1, 1063],
limit: 20, limit: 20,
@ -27,13 +29,14 @@ export default function RepliesList({ id }: { id: string }) {
READONLY_RELAYS, READONLY_RELAYS,
(event: any) => { (event: any) => {
next(null, (prev: any) => (prev ? [...prev, event] : [event])); next(null, (prev: any) => (prev ? [...prev, event] : [event]));
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
return ( return (
<div className="mt-5"> <div className="mt-5">
@ -46,12 +49,12 @@ export default function RepliesList({ id }: { id: string }) {
{error && <div>failed to load</div>} {error && <div>failed to load</div>}
{!data ? ( {!data ? (
<div className="flex gap-2 px-3 py-4"> <div className="flex gap-2 px-3 py-4">
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div> <div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800" />
<div className="flex w-full flex-1 flex-col justify-center gap-1"> <div className="flex w-full flex-1 flex-col justify-center gap-1">
<div className="flex items-baseline gap-2 text-sm"> <div className="flex items-baseline gap-2 text-sm">
<div className="h-2.5 w-20 animate-pulse rounded-sm bg-zinc-800"></div> <div className="h-2.5 w-20 animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
<div className="h-4 w-44 animate-pulse rounded-sm bg-zinc-800"></div> <div className="h-4 w-44 animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
</div> </div>
) : ( ) : (

View File

@ -1,18 +1,18 @@
import { Kind1 } from '@app/note/components/kind1'; import { Kind1 } from "@app/note/components/kind1";
import { Kind1063 } from '@app/note/components/kind1063'; import { Kind1063 } from "@app/note/components/kind1063";
import NoteMetadata from '@app/note/components/metadata'; import NoteMetadata from "@app/note/components/metadata";
import { NoteSkeleton } from '@app/note/components/skeleton'; import { NoteSkeleton } from "@app/note/components/skeleton";
import { NoteDefaultUser } from '@app/note/components/user/default'; import { NoteDefaultUser } from "@app/note/components/user/default";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { noteParser } from '@utils/parser'; import { noteParser } from "@utils/parser";
import { memo, useContext } from 'react'; import { memo, useContext } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
function isJSON(str: string) { function isJSON(str: string) {
try { try {
@ -23,11 +23,16 @@ function isJSON(str: string) {
return true; return true;
} }
export const RootNote = memo(function RootNote({ id, fallback }: { id: string; fallback?: any }) { export const RootNote = memo(function RootNote({
id,
fallback,
}: { id: string; fallback?: any }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const parseFallback = isJSON(fallback) ? JSON.parse(fallback) : null; const parseFallback = isJSON(fallback) ? JSON.parse(fallback) : null;
const { data, error } = useSWRSubscription(parseFallback ? null : id, (key, { next }) => { const { data, error } = useSWRSubscription(
parseFallback ? null : id,
(key, { next }) => {
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
{ {
@ -42,13 +47,14 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
undefined, undefined,
{ {
unsubscribeOnEose: true, unsubscribeOnEose: true,
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
const openNote = (e) => { const openNote = (e) => {
const selection = window.getSelection(); const selection = window.getSelection();
@ -66,18 +72,24 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
const contentFallback = noteParser(parseFallback); const contentFallback = noteParser(parseFallback);
return ( return (
<div onClick={(e) => openNote(e)} className="flex flex-col px-3"> <div onKeyDown={(e) => openNote(e)} className="flex flex-col px-3">
<NoteDefaultUser pubkey={parseFallback.pubkey} time={parseFallback.created_at} /> <NoteDefaultUser
pubkey={parseFallback.pubkey}
time={parseFallback.created_at}
/>
<div className="mt-3 pl-[46px]"> <div className="mt-3 pl-[46px]">
<Kind1 content={contentFallback} /> <Kind1 content={contentFallback} />
<NoteMetadata id={parseFallback.id} eventPubkey={parseFallback.pubkey} /> <NoteMetadata
id={parseFallback.id}
eventPubkey={parseFallback.pubkey}
/>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div onClick={(e) => openNote(e)} className="flex flex-col px-3"> <div onKeyDown={(e) => openNote(e)} className="flex flex-col px-3">
{data ? ( {data ? (
<> <>
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} /> <NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />

View File

@ -1,25 +1,30 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants'; import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import { Popover, Transition } from '@headlessui/react'; import { Popover, Transition } from "@headlessui/react";
import dayjs from 'dayjs'; import dayjs from "dayjs";
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from "dayjs/plugin/relativeTime";
import { Fragment } from 'react'; import { Fragment } from "react";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export function NoteDefaultUser({ pubkey, time }: { pubkey: string; time: number }) { export function NoteDefaultUser({
pubkey,
time,
}: { pubkey: string; time: number }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
return ( return (
<Popover className="relative flex items-center gap-2.5"> <Popover className="relative flex items-center gap-2.5">
<Popover.Button className="h-9 w-9 shrink-0 overflow-hidden rounded-md bg-zinc-900"> <Popover.Button className="h-9 w-9 shrink-0 overflow-hidden rounded-md bg-zinc-900">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${
user?.picture ? user.picture : DEFAULT_AVATAR
}`}
alt={pubkey} alt={pubkey}
className="h-9 w-9 object-cover" className="h-9 w-9 object-cover"
/> />
@ -27,7 +32,9 @@ export function NoteDefaultUser({ pubkey, time }: { pubkey: string; time: number
<div className="flex w-full flex-1 items-start justify-between"> <div className="flex w-full flex-1 items-start justify-between">
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<h5 className="text-sm font-semibold leading-none"> <h5 className="text-sm font-semibold leading-none">
{user?.display_name || user?.name || <div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700"></div>} {user?.display_name || user?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)}
</h5> </h5>
<div className="flex items-baseline gap-1.5 text-sm leading-none text-zinc-500"> <div className="flex items-baseline gap-1.5 text-sm leading-none text-zinc-500">
<span>{user?.nip05 || shortenKey(pubkey)}</span> <span>{user?.nip05 || shortenKey(pubkey)}</span>
@ -47,12 +54,14 @@ export function NoteDefaultUser({ pubkey, time }: { pubkey: string; time: number
> >
<Popover.Panel className="absolute left-0 top-8 z-10 mt-3 w-screen max-w-sm px-4 sm:px-0 lg:max-w-3xl"> <Popover.Panel className="absolute left-0 top-8 z-10 mt-3 w-screen max-w-sm px-4 sm:px-0 lg:max-w-3xl">
<div <div
onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
className="w-full max-w-xs overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 shadow-input ring-1 ring-black ring-opacity-5" className="w-full max-w-xs overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 shadow-input ring-1 ring-black ring-opacity-5"
> >
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3"> <div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:200:200/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:200:200/plain/${
user?.picture ? user.picture : DEFAULT_AVATAR
}`}
alt={pubkey} alt={pubkey}
className="h-14 w-14 shrink-0 rounded-lg object-cover" className="h-14 w-14 shrink-0 rounded-lg object-cover"
/> />
@ -60,7 +69,7 @@ export function NoteDefaultUser({ pubkey, time }: { pubkey: string; time: number
<div className="inline-flex w-2/3 flex-col gap-0.5"> <div className="inline-flex w-2/3 flex-col gap-0.5">
<h5 className="text-sm font-semibold leading-none"> <h5 className="text-sm font-semibold leading-none">
{user?.display_name || user?.name || ( {user?.display_name || user?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700"></div> <div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)} )}
</h5> </h5>
<span className="truncate text-sm leading-none text-zinc-500"> <span className="truncate text-sm leading-none text-zinc-500">
@ -68,7 +77,9 @@ export function NoteDefaultUser({ pubkey, time }: { pubkey: string; time: number
</span> </span>
</div> </div>
<div> <div>
<p className="line-clamp-3 text-sm leading-tight text-zinc-100">{user?.about}</p> <p className="line-clamp-3 text-sm leading-tight text-zinc-100">
{user?.about}
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,23 +1,28 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants'; import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import dayjs from 'dayjs'; import dayjs from "dayjs";
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export default function NoteReplyUser({ pubkey, time }: { pubkey: string; time: number }) { export default function NoteReplyUser({
pubkey,
time,
}: { pubkey: string; time: number }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
return ( return (
<div className="group flex items-start gap-2.5"> <div className="group flex items-start gap-2.5">
<div className="relative h-9 w-9 shrink-0 rounded-md"> <div className="relative h-9 w-9 shrink-0 rounded-md">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${
user?.picture ? user.picture : DEFAULT_AVATAR
}`}
alt={pubkey} alt={pubkey}
className="h-9 w-9 rounded-md object-cover" className="h-9 w-9 rounded-md object-cover"
/> />
@ -28,7 +33,9 @@ export default function NoteReplyUser({ pubkey, time }: { pubkey: string; time:
{user?.display_name || user?.name || shortenKey(pubkey)} {user?.display_name || user?.name || shortenKey(pubkey)}
</span> </span>
<span className="leading-none text-zinc-500">·</span> <span className="leading-none text-zinc-500">·</span>
<span className="leading-none text-zinc-500">{dayjs().to(dayjs.unix(time), true)}</span> <span className="leading-none text-zinc-500">
{dayjs().to(dayjs.unix(time), true)}
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,39 +1,48 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants'; import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from '@utils/shortenKey'; import { shortenKey } from "@utils/shortenKey";
import { Popover, Transition } from '@headlessui/react'; import { Popover, Transition } from "@headlessui/react";
import dayjs from 'dayjs'; import dayjs from "dayjs";
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from "dayjs/plugin/relativeTime";
import { Fragment } from 'react'; import { Fragment } from "react";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export function NoteRepostUser({ pubkey, time }: { pubkey: string; time: number }) { export function NoteRepostUser({
pubkey,
time,
}: { pubkey: string; time: number }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
return ( return (
<Popover className="relative flex items-center gap-2.5"> <Popover className="relative flex items-center gap-2.5">
<Popover.Button className="h-9 w-9 shrink-0 overflow-hidden rounded-md bg-zinc-900"> <Popover.Button className="h-9 w-9 shrink-0 overflow-hidden rounded-md bg-zinc-900">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${
user?.picture ? user.picture : DEFAULT_AVATAR
}`}
alt={pubkey} alt={pubkey}
className="h-9 w-9 rounded-md object-cover" className="h-9 w-9 rounded-md object-cover"
/> />
</Popover.Button> </Popover.Button>
<div className="flex items-baseline gap-1.5 text-sm"> <div className="flex items-baseline gap-1.5 text-sm">
<h5 className="font-semibold leading-tight group-hover:underline"> <h5 className="font-semibold leading-tight group-hover:underline">
{user?.display_name || user?.name || <div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700"></div>} {user?.display_name || user?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)}
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
{' '} {" "}
reposted reposted
</span> </span>
</h5> </h5>
<span className="leading-tight text-zinc-500">·</span> <span className="leading-tight text-zinc-500">·</span>
<span className="text-zinc-500">{dayjs().to(dayjs.unix(time), true)}</span> <span className="text-zinc-500">
{dayjs().to(dayjs.unix(time), true)}
</span>
</div> </div>
<Transition <Transition
as={Fragment} as={Fragment}
@ -46,12 +55,14 @@ export function NoteRepostUser({ pubkey, time }: { pubkey: string; time: number
> >
<Popover.Panel className="absolute left-0 top-8 z-10 mt-3 w-screen max-w-sm px-4 sm:px-0 lg:max-w-3xl"> <Popover.Panel className="absolute left-0 top-8 z-10 mt-3 w-screen max-w-sm px-4 sm:px-0 lg:max-w-3xl">
<div <div
onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
className="w-full max-w-xs overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 shadow-input ring-1 ring-black ring-opacity-5" className="w-full max-w-xs overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 shadow-input ring-1 ring-black ring-opacity-5"
> >
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3"> <div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:200:200/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:200:200/plain/${
user?.picture ? user.picture : DEFAULT_AVATAR
}`}
alt={pubkey} alt={pubkey}
className="h-14 w-14 shrink-0 rounded-lg object-cover" className="h-14 w-14 shrink-0 rounded-lg object-cover"
/> />
@ -59,7 +70,7 @@ export function NoteRepostUser({ pubkey, time }: { pubkey: string; time: number
<div className="inline-flex w-2/3 flex-col gap-0.5"> <div className="inline-flex w-2/3 flex-col gap-0.5">
<h5 className="text-sm font-semibold leading-none"> <h5 className="text-sm font-semibold leading-none">
{user?.display_name || user?.name || ( {user?.display_name || user?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700"></div> <div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)} )}
</h5> </h5>
<span className="truncate text-sm leading-none text-zinc-500"> <span className="truncate text-sm leading-none text-zinc-500">
@ -67,7 +78,9 @@ export function NoteRepostUser({ pubkey, time }: { pubkey: string; time: number
</span> </span>
</div> </div>
<div> <div>
<p className="line-clamp-3 text-sm leading-tight text-zinc-100">{user?.about}</p> <p className="line-clamp-3 text-sm leading-tight text-zinc-100">
{user?.about}
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { navigate } from 'vite-plugin-ssr/client/router'; import { navigate } from "vite-plugin-ssr/client/router";
export function NoteWrapper({ export function NoteWrapper({
children, children,
@ -19,7 +19,7 @@ export function NoteWrapper({
}; };
return ( return (
<div onClick={(event) => openThread(event, href)} className={className}> <div onKeyDown={(event) => openThread(event, href)} className={className}>
{children} {children}
</div> </div>
); );

View File

@ -1,6 +1,6 @@
import AppHeader from '@shared/appHeader'; import AppHeader from "@shared/appHeader";
import MultiAccounts from '@shared/multiAccounts'; import MultiAccounts from "@shared/multiAccounts";
import Navigation from '@shared/navigation'; import Navigation from "@shared/navigation";
export function LayoutNewsfeed({ children }: { children: React.ReactNode }) { export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
return ( return (
@ -20,7 +20,9 @@ export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900"> <div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
<Navigation /> <Navigation />
</div> </div>
<div className="col-span-3 overflow-hidden xl:col-span-4">{children}</div> <div className="col-span-3 overflow-hidden xl:col-span-4">
{children}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,17 +1,17 @@
import { Kind1 } from '@app/note/components/kind1'; import { Kind1 } from "@app/note/components/kind1";
import NoteMetadata from '@app/note/components/metadata'; import NoteMetadata from "@app/note/components/metadata";
import RepliesList from '@app/note/components/replies/list'; import RepliesList from "@app/note/components/replies/list";
import { NoteDefaultUser } from '@app/note/components/user/default'; import { NoteDefaultUser } from "@app/note/components/user/default";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { usePageContext } from '@utils/hooks/usePageContext'; import { usePageContext } from "@utils/hooks/usePageContext";
import { noteParser } from '@utils/parser'; import { noteParser } from "@utils/parser";
import { useContext } from 'react'; import { useContext } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -20,7 +20,9 @@ export function Page() {
const noteID = searchParams.id; const noteID = searchParams.id;
const { data, error } = useSWRSubscription(noteID ? ['note', noteID] : null, ([, key], { next }) => { const { data, error } = useSWRSubscription(
noteID ? ["note", noteID] : null,
([, key], { next }) => {
// subscribe to note // subscribe to note
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
[ [
@ -31,13 +33,14 @@ export function Page() {
READONLY_RELAYS, READONLY_RELAYS,
(event: any) => { (event: any) => {
next(null, event); next(null, event);
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
const content = !error && data ? noteParser(data) : null; const content = !error && data ? noteParser(data) : null;

View File

@ -1 +1 @@
export { LayoutNewsfeed as Layout } from './layout'; export { LayoutNewsfeed as Layout } from "./layout";

View File

@ -1,13 +1,22 @@
import { CreateViewModal } from '@app/today/components/views/createModal'; import { CreateViewModal } from "@app/today/components/views/createModal";
export function Header() { export function Header() {
return ( return (
<div className="flex w-full gap-4"> <div className="flex w-full gap-4">
<button className="from-zinc-90 inline-flex h-11 items-center overflow-hidden border-b border-fuchsia-500 hover:bg-zinc-900"> <button
<span className="px-2 text-sm font-semibold text-zinc-300">Following</span> type="button"
className="from-zinc-90 inline-flex h-11 items-center overflow-hidden border-b border-fuchsia-500 hover:bg-zinc-900"
>
<span className="px-2 text-sm font-semibold text-zinc-300">
Following
</span>
</button> </button>
<div className="flex h-11 items-center -space-x-1 overflow-hidden"> <div className="flex h-11 items-center -space-x-1 overflow-hidden">
<img className="inline-block h-6 w-6 rounded ring-2 ring-zinc-950" src="https://133332.xyz/p.jpg" alt="" /> <img
className="inline-block h-6 w-6 rounded ring-2 ring-zinc-950"
src="https://133332.xyz/p.jpg"
alt=""
/>
<img <img
className="inline-block h-6 w-6 rounded ring-2 ring-zinc-950" className="inline-block h-6 w-6 rounded ring-2 ring-zinc-950"
src="https://void.cat/d/3Bp6jSHURFNQ9u3pK8nwtq.webp" src="https://void.cat/d/3Bp6jSHURFNQ9u3pK8nwtq.webp"
@ -37,7 +46,11 @@ export function Header() {
/> />
</div> </div>
<div className="flex h-11 items-center overflow-hidden"> <div className="flex h-11 items-center overflow-hidden">
<img className="ring-zinc-95 ring-20 inline-block h-6 w-6 rounded" src="http://nostr.build/i/6369.jpg" alt="" /> <img
className="ring-zinc-95 ring-20 inline-block h-6 w-6 rounded"
src="http://nostr.build/i/6369.jpg"
alt=""
/>
</div> </div>
<CreateViewModal /> <CreateViewModal />
</div> </div>

View File

@ -1,8 +1,8 @@
import CancelIcon from '@icons/cancel'; import CancelIcon from "@icons/cancel";
import PlusIcon from '@icons/plus'; import PlusIcon from "@icons/plus";
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from 'react'; import { Fragment, useState } from "react";
export function CreateViewModal() { export function CreateViewModal() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -63,19 +63,22 @@ export function CreateViewModal() {
<button <button
type="button" type="button"
onClick={closeModal} onClick={closeModal}
autoFocus={false}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon width={16} height={16} className="text-zinc-400" /> <CancelIcon
width={16}
height={16}
className="text-zinc-400"
/>
</button> </button>
</div> </div>
<Dialog.Description className="text-sm leading-tight text-zinc-400"> <Dialog.Description className="text-sm leading-tight text-zinc-400">
View is specific feature help you pin who you want to see in your feed. You can add maximum 5 View is specific feature help you pin who you want to see
people in a view. in your feed. You can add maximum 5 people in a view.
</Dialog.Description> </Dialog.Description>
</div> </div>
</div> </div>
<div className="flex h-full w-full flex-col overflow-y-auto pb-5 pt-3"></div> <div className="flex h-full w-full flex-col overflow-y-auto pb-5 pt-3" />
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -1,6 +1,6 @@
import AppHeader from '@shared/appHeader'; import AppHeader from "@shared/appHeader";
import MultiAccounts from '@shared/multiAccounts'; import MultiAccounts from "@shared/multiAccounts";
import Navigation from '@shared/navigation'; import Navigation from "@shared/navigation";
export function LayoutNewsfeed({ children }: { children: React.ReactNode }) { export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
return ( return (
@ -20,7 +20,9 @@ export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900"> <div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
<Navigation /> <Navigation />
</div> </div>
<div className="col-span-3 overflow-hidden xl:col-span-4">{children}</div> <div className="col-span-3 overflow-hidden xl:col-span-4">
{children}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,20 +1,28 @@
import { NoteBase } from '@app/note/components/base'; import { NoteBase } from "@app/note/components/base";
import { NoteQuoteRepost } from '@app/note/components/quoteRepost'; import { NoteQuoteRepost } from "@app/note/components/quoteRepost";
import { NoteSkeleton } from '@app/note/components/skeleton'; import { NoteSkeleton } from "@app/note/components/skeleton";
import { Header } from '@app/today/components/header'; import { Header } from "@app/today/components/header";
import { getNotes } from '@utils/storage'; import { getNotes } from "@utils/storage";
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from "@tanstack/react-query";
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from "@tanstack/react-virtual";
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from "react";
const ITEM_PER_PAGE = 10; const ITEM_PER_PAGE = 10;
const TIME = Math.floor(Date.now() / 1000); const TIME = Math.floor(Date.now() / 1000);
export function Page() { export function Page() {
const { status, error, data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage }: any = useInfiniteQuery({ const {
queryKey: ['following'], status,
error,
data,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
}: any = useInfiniteQuery({
queryKey: ["following"],
queryFn: async ({ pageParam = 0 }) => { queryFn: async ({ pageParam = 0 }) => {
return await getNotes(TIME, ITEM_PER_PAGE, pageParam); return await getNotes(TIME, ITEM_PER_PAGE, pageParam);
}, },
@ -40,7 +48,11 @@ export function Page() {
return; return;
} }
if (lastItem.index >= allRows.length - 1 && hasNextPage && !isFetchingNextPage) { if (
lastItem.index >= allRows.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage(); fetchNextPage();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -50,19 +62,19 @@ export function Page() {
<div <div
ref={parentRef} ref={parentRef}
className="scrollbar-hide flex h-full flex-col justify-between gap-1.5 overflow-y-auto" className="scrollbar-hide flex h-full flex-col justify-between gap-1.5 overflow-y-auto"
style={{ contain: 'strict' }} style={{ contain: "strict" }}
> >
<div className="flex h-11 w-full shrink-0 items-center justify-between border-b border-zinc-900 px-3"> <div className="flex h-11 w-full shrink-0 items-center justify-between border-b border-zinc-900 px-3">
<Header /> <Header />
</div> </div>
<div className="flex-1"> <div className="flex-1">
{status === 'loading' ? ( {status === "loading" ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-md border border-zinc-800 bg-zinc-900 px-3 py-3 shadow-input shadow-black/20"> <div className="rounded-md border border-zinc-800 bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : status === 'error' ? ( ) : status === "error" ? (
<div>{error.message}</div> <div>{error.message}</div>
) : ( ) : (
<div <div
@ -74,7 +86,10 @@ export function Page() {
<div <div
className="absolute left-0 top-0 w-full" className="absolute left-0 top-0 w-full"
style={{ style={{
transform: `translateY(${itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin}px)`, transform: `translateY(${
itemsVirtualizer[0].start -
rowVirtualizer.options.scrollMargin
}px)`,
}} }}
> >
{rowVirtualizer.getVirtualItems().map((virtualRow) => { {rowVirtualizer.getVirtualItems().map((virtualRow) => {
@ -82,13 +97,21 @@ export function Page() {
if (note) { if (note) {
if (note.kind === 1) { if (note.kind === 1) {
return ( return (
<div key={virtualRow.index} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}> <div
key={virtualRow.index}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
>
<NoteBase key={note.event_id} event={note} /> <NoteBase key={note.event_id} event={note} />
</div> </div>
); );
} else { } else {
return ( return (
<div key={virtualRow.index} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}> <div
key={virtualRow.index}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
>
<NoteQuoteRepost key={note.event_id} event={note} /> <NoteQuoteRepost key={note.event_id} event={note} />
</div> </div>
); );

View File

@ -1,10 +1,10 @@
import { StrictMode } from 'react'; import { StrictMode } from "react";
import { Root, createRoot, hydrateRoot } from 'react-dom/client'; import { Root, createRoot, hydrateRoot } from "react-dom/client";
import 'vidstack/styles/defaults.css'; import "vidstack/styles/defaults.css";
import './index.css'; import "./index.css";
import { Shell } from './shell'; import { Shell } from "./shell";
import { PageContextClient } from './types'; import { PageContextClient } from "./types";
export const clientRouting = true; export const clientRouting = true;
export const hydrationCanBeAborted = true; export const hydrationCanBeAborted = true;
@ -14,7 +14,10 @@ let root: Root;
export async function render(pageContext: PageContextClient) { export async function render(pageContext: PageContextClient) {
const { Page, pageProps } = pageContext; const { Page, pageProps } = pageContext;
if (!Page) throw new Error('Client-side render() hook expects pageContext.Page to be defined'); if (!Page)
throw new Error(
"Client-side render() hook expects pageContext.Page to be defined",
);
const page = ( const page = (
<StrictMode> <StrictMode>
@ -24,9 +27,9 @@ export async function render(pageContext: PageContextClient) {
</StrictMode> </StrictMode>
); );
const container = document.getElementById('app'); const container = document.getElementById("app");
// SPA // SPA
if (container.innerHTML === '' || !pageContext.isHydration) { if (container.innerHTML === "" || !pageContext.isHydration) {
if (!root) { if (!root) {
root = createRoot(container); root = createRoot(container);
} }

View File

@ -1,29 +1,32 @@
import { StrictMode } from 'react'; import { StrictMode } from "react";
import ReactDOMServer from 'react-dom/server'; import ReactDOMServer from "react-dom/server";
import { dangerouslySkipEscape, escapeInject } from 'vite-plugin-ssr/server'; import { dangerouslySkipEscape, escapeInject } from "vite-plugin-ssr/server";
import { Shell } from './shell'; import { Shell } from "./shell";
import { PageContextServer } from './types'; import { PageContextServer } from "./types";
export const passToClient = ['pageProps']; export const passToClient = ["pageProps"];
export function render(pageContext: PageContextServer) { export function render(pageContext: PageContextServer) {
let pageHtml: string; let pageHtml: string;
if (!pageContext.Page) { if (!pageContext.Page) {
// SPA // SPA
pageHtml = ''; pageHtml = "";
} else { } else {
// SSR / HTML-only // SSR / HTML-only
const { Page, pageProps } = pageContext; const { Page, pageProps } = pageContext;
if (!Page) throw new Error('My render() hook expects pageContext.Page to be defined'); if (!Page)
throw new Error(
"My render() hook expects pageContext.Page to be defined",
);
pageHtml = ReactDOMServer.renderToString( pageHtml = ReactDOMServer.renderToString(
<StrictMode> <StrictMode>
<Shell pageContext={pageContext}> <Shell pageContext={pageContext}>
<Page {...pageProps} /> <Page {...pageProps} />
</Shell> </Shell>
</StrictMode> </StrictMode>,
); );
} }

View File

@ -1,3 +1,7 @@
export function LayoutDefault({ children }: { children: React.ReactNode }) { export function LayoutDefault({ children }: { children: React.ReactNode }) {
return <div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">{children}</div>; return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
{children}
</div>
);
} }

View File

@ -1,22 +1,29 @@
import { RelayProvider } from '@shared/relayProvider'; import { RelayProvider } from "@shared/relayProvider";
import { PageContextProvider } from '@utils/hooks/usePageContext'; import { PageContextProvider } from "@utils/hooks/usePageContext";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { LayoutDefault } from './layoutDefault'; import { LayoutDefault } from "./layoutDefault";
import { PageContext } from './types'; import { PageContext } from "./types";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
export function Shell({ children, pageContext }: { children: React.ReactNode; pageContext: PageContext }) { export function Shell({
const Layout = (pageContext.exports.Layout as React.ElementType) || (LayoutDefault as React.ElementType); children,
pageContext,
}: { children: React.ReactNode; pageContext: PageContext }) {
const Layout =
(pageContext.exports.Layout as React.ElementType) ||
(LayoutDefault as React.ElementType);
return ( return (
<PageContextProvider pageContext={pageContext}> <PageContextProvider pageContext={pageContext}>
<RelayProvider> <RelayProvider>
<Layout> <Layout>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> <QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</Layout> </Layout>
</RelayProvider> </RelayProvider>
</PageContextProvider> </PageContextProvider>

View File

@ -6,7 +6,7 @@ PageContextBuiltInClientWithClientRouting as PageContextBuiltInClient
/*/ /*/
// When using Server Routing // When using Server Routing
PageContextBuiltInClientWithServerRouting as PageContextBuiltInClient, //*/ PageContextBuiltInClientWithServerRouting as PageContextBuiltInClient, //*/
} from 'vite-plugin-ssr/types'; } from "vite-plugin-ssr/types";
export type { PageContextServer }; export type { PageContextServer };
export type { PageContextClient }; export type { PageContextClient };

View File

@ -1,12 +1,15 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
export default function ActiveAccount({ user }: { user: any }) { export default function ActiveAccount({ user }: { user: any }) {
const userData = JSON.parse(user.metadata); const userData = JSON.parse(user.metadata);
return ( return (
<button className="relative h-10 w-10 overflow-hidden rounded-lg"> <button
type="button"
className="relative h-10 w-10 overflow-hidden rounded-lg"
>
<Image <Image
src={userData.picture || DEFAULT_AVATAR} src={userData.picture || DEFAULT_AVATAR}
alt="user's avatar" alt="user's avatar"

View File

@ -1,6 +1,6 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from "@stores/constants";
export default function InactiveAccount({ user }: { user: any }) { export default function InactiveAccount({ user }: { user: any }) {
const userData = JSON.parse(user.metadata); const userData = JSON.parse(user.metadata);

View File

@ -1,6 +1,6 @@
import { usePageContext } from '@utils/hooks/usePageContext'; import { usePageContext } from "@utils/hooks/usePageContext";
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export default function ActiveLink({ export default function ActiveLink({
href, href,
@ -17,7 +17,10 @@ export default function ActiveLink({
const pathName = pageContext.urlPathname; const pathName = pageContext.urlPathname;
return ( return (
<a href={href} className={twMerge(className, href === pathName ? activeClassName : '')}> <a
href={href}
className={twMerge(className, href === pathName ? activeClassName : "")}
>
{children} {children}
</a> </a>
); );

View File

@ -1,6 +1,6 @@
import ArrowLeftIcon from '@icons/arrowLeft'; import ArrowLeftIcon from "@icons/arrowLeft";
import ArrowRightIcon from '@icons/arrowRight'; import ArrowRightIcon from "@icons/arrowRight";
import RefreshIcon from '@icons/refresh'; import RefreshIcon from "@icons/refresh";
export default function AppHeader() { export default function AppHeader() {
const goBack = () => { const goBack = () => {
@ -16,37 +16,57 @@ export default function AppHeader() {
}; };
return ( return (
<div data-tauri-drag-region className="flex h-full w-full flex-1 items-center px-2"> <div
<div data-tauri-drag-region className="flex w-full items-center justify-center gap-2"> data-tauri-drag-region
className="flex h-full w-full flex-1 items-center px-2"
>
<div
data-tauri-drag-region
className="flex w-full items-center justify-center gap-2"
>
<div className="flex h-full items-center gap-2"> <div className="flex h-full items-center gap-2">
<button <button
type="button"
onClick={() => goBack()} onClick={() => goBack()}
className="group inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="group inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<ArrowLeftIcon width={14} height={14} className="text-zinc-500 group-hover:text-zinc-300" /> <ArrowLeftIcon
width={14}
height={14}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button> </button>
<button <button
type="button"
onClick={() => goForward()} onClick={() => goForward()}
className="group inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="group inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<ArrowRightIcon width={14} height={14} className="text-zinc-500 group-hover:text-zinc-300" /> <ArrowRightIcon
width={14}
height={14}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button> </button>
</div> </div>
<div> <div>
<input <input
autoCapitalize="none" autoCapitalize="none"
autoCorrect="off" autoCorrect="off"
autoFocus={false}
placeholder="Search..." placeholder="Search..."
className="h-6 w-[453px] rounded border border-zinc-800 bg-zinc-900 px-2.5 text-center text-[11px] text-sm leading-5 text-zinc-500 placeholder:leading-5 placeholder:text-zinc-600 focus:outline-none" className="h-6 w-[453px] rounded border border-zinc-800 bg-zinc-900 px-2.5 text-center text-[11px] text-sm leading-5 text-zinc-500 placeholder:leading-5 placeholder:text-zinc-600 focus:outline-none"
/> />
</div> </div>
<div className="flex h-full items-center gap-2"> <div className="flex h-full items-center gap-2">
<button <button
type="button"
onClick={() => reload()} onClick={() => reload()}
className="group inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="group inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<RefreshIcon width={14} height={14} className="text-zinc-500 group-hover:text-zinc-300" /> <RefreshIcon
width={14}
height={14}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
import { createBlobFromFile } from '@utils/createBlobFromFile'; import { createBlobFromFile } from "@utils/createBlobFromFile";
import { open } from '@tauri-apps/api/dialog'; import { open } from "@tauri-apps/api/dialog";
import { Body, fetch } from '@tauri-apps/api/http'; import { Body, fetch } from "@tauri-apps/api/http";
import { useState } from 'react'; import { useState } from "react";
export function AvatarUploader({ valueState }: { valueState: any }) { export function AvatarUploader({ valueState }: { valueState: any }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -12,8 +12,8 @@ export function AvatarUploader({ valueState }: { valueState: any }) {
multiple: false, multiple: false,
filters: [ filters: [
{ {
name: 'Image', name: "Image",
extensions: ['png', 'jpeg', 'jpg', 'gif'], extensions: ["png", "jpeg", "jpg", "gif"],
}, },
], ],
}); });
@ -24,23 +24,26 @@ export function AvatarUploader({ valueState }: { valueState: any }) {
} else { } else {
setLoading(true); setLoading(true);
const filename = selected.split('/').pop(); const filename = selected.split("/").pop();
const file = await createBlobFromFile(selected); const file = await createBlobFromFile(selected);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
const res: { data: { file: { id: string } } } = await fetch('https://void.cat/upload?cli=false', { const res: { data: { file: { id: string } } } = await fetch(
method: 'POST', "https://void.cat/upload?cli=false",
{
method: "POST",
timeout: 5, timeout: 5,
headers: { headers: {
accept: '*/*', accept: "*/*",
'Content-Type': 'application/octet-stream', "Content-Type": "application/octet-stream",
'V-Filename': filename, "V-Filename": filename,
'V-Description': 'Upload from https://lume.nu', "V-Description": "Upload from https://lume.nu",
'V-Strip-Metadata': 'true', "V-Strip-Metadata": "true",
}, },
body: Body.bytes(buf), body: Body.bytes(buf),
}); },
const webpImage = 'https://void.cat/d/' + res.data.file.id + '.webp'; );
const webpImage = `https://void.cat/d/${res.data.file.id}.webp`;
valueState(webpImage); valueState(webpImage);
setLoading(false); setLoading(false);
@ -60,12 +63,20 @@ export function AvatarUploader({ valueState }: { valueState: any }) {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
<span className="leading-none">Upload</span> <span className="leading-none">Upload</span>

View File

@ -1,43 +1,46 @@
import PlusCircleIcon from '@shared/icons/plusCircle'; import PlusCircleIcon from "@shared/icons/plusCircle";
import { createBlobFromFile } from '@utils/createBlobFromFile'; import { createBlobFromFile } from "@utils/createBlobFromFile";
import { open } from '@tauri-apps/api/dialog'; import { open } from "@tauri-apps/api/dialog";
import { listen } from '@tauri-apps/api/event'; import { listen } from "@tauri-apps/api/event";
import { Body, fetch } from '@tauri-apps/api/http'; import { Body, fetch } from "@tauri-apps/api/http";
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from "react";
import { Transforms } from 'slate'; import { Transforms } from "slate";
import { useSlateStatic } from 'slate-react'; import { useSlateStatic } from "slate-react";
export function ImageUploader() { export function ImageUploader() {
const editor = useSlateStatic(); const editor = useSlateStatic();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const insertImage = (editor, url) => { const insertImage = (editor, url) => {
const image = { type: 'image', url, children: [{ text: url }] }; const image = { type: "image", url, children: [{ text: url }] };
Transforms.insertNodes(editor, image); Transforms.insertNodes(editor, image);
}; };
const uploadToVoidCat = useCallback( const uploadToVoidCat = useCallback(
async (filepath) => { async (filepath) => {
const filename = filepath.split('/').pop(); const filename = filepath.split("/").pop();
const file = await createBlobFromFile(filepath); const file = await createBlobFromFile(filepath);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
try { try {
const res: { data: { file: { id: string } } } = await fetch('https://void.cat/upload?cli=false', { const res: { data: { file: { id: string } } } = await fetch(
method: 'POST', "https://void.cat/upload?cli=false",
{
method: "POST",
timeout: 5, timeout: 5,
headers: { headers: {
accept: '*/*', accept: "*/*",
'Content-Type': 'application/octet-stream', "Content-Type": "application/octet-stream",
'V-Filename': filename, "V-Filename": filename,
'V-Description': 'Uploaded from https://lume.nu', "V-Description": "Uploaded from https://lume.nu",
'V-Strip-Metadata': 'true', "V-Strip-Metadata": "true",
}, },
body: Body.bytes(buf), body: Body.bytes(buf),
}); },
const image = 'https://void.cat/d/' + res.data.file.id + '.webp'; );
const image = `https://void.cat/d/${res.data.file.id}.webp`;
// update parent state // update parent state
insertImage(editor, image); insertImage(editor, image);
// reset loading state // reset loading state
@ -48,13 +51,13 @@ export function ImageUploader() {
// handle error // handle error
if (error instanceof SyntaxError) { if (error instanceof SyntaxError) {
// Unexpected token < in JSON // Unexpected token < in JSON
console.log('There was a SyntaxError', error); console.log("There was a SyntaxError", error);
} else { } else {
console.log('There was an error', error); console.log("There was an error", error);
} }
} }
}, },
[editor] [editor],
); );
const openFileDialog = async () => { const openFileDialog = async () => {
@ -62,8 +65,8 @@ export function ImageUploader() {
multiple: false, multiple: false,
filters: [ filters: [
{ {
name: 'Image', name: "Image",
extensions: ['png', 'jpeg', 'jpg', 'gif'], extensions: ["png", "jpeg", "jpg", "gif"],
}, },
], ],
}); });
@ -80,7 +83,7 @@ export function ImageUploader() {
useEffect(() => { useEffect(() => {
async function initFileDrop() { async function initFileDrop() {
const unlisten = await listen('tauri://file-drop', (event) => { const unlisten = await listen("tauri://file-drop", (event) => {
// set loading state // set loading state
setLoading(true); setLoading(true);
// upload file // upload file
@ -98,7 +101,6 @@ export function ImageUploader() {
return ( return (
<button <button
type="button" type="button"
autoFocus={false}
onClick={() => openFileDialog()} onClick={() => openFileDialog()}
className="inline-flex h-8 w-8 items-center justify-center rounded hover:bg-zinc-800" className="inline-flex h-8 w-8 items-center justify-center rounded hover:bg-zinc-800"
> >
@ -109,12 +111,20 @@ export function ImageUploader() {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
<PlusCircleIcon width={20} height={20} className="text-zinc-500" /> <PlusCircleIcon width={20} height={20} className="text-zinc-500" />

View File

@ -1,18 +1,18 @@
import { Post } from '@shared/composer/types/post'; import { Post } from "@shared/composer/types/post";
import { User } from '@shared/composer/user'; import { User } from "@shared/composer/user";
import CancelIcon from '@icons/cancel'; import CancelIcon from "@icons/cancel";
import ChevronDownIcon from '@icons/chevronDown'; import ChevronDownIcon from "@icons/chevronDown";
import ChevronRightIcon from '@icons/chevronRight'; import ChevronRightIcon from "@icons/chevronRight";
import ComposeIcon from '@icons/compose'; import ComposeIcon from "@icons/compose";
import { composerAtom } from '@stores/composer'; import { composerAtom } from "@stores/composer";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from 'jotai'; import { useAtom } from "jotai";
import { Fragment, useState } from 'react'; import { Fragment, useState } from "react";
export function ComposerModal() { export function ComposerModal() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -32,7 +32,6 @@ export function ComposerModal() {
<> <>
<button <button
type="button" type="button"
autoFocus={false}
onClick={() => openModal()} onClick={() => openModal()}
className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-md bg-fuchsia-500 px-2.5 text-xs font-medium text-zinc-200 shadow-button hover:bg-fuchsia-600 focus:outline-none" className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-md bg-fuchsia-500 px-2.5 text-xs font-medium text-zinc-200 shadow-button hover:bg-fuchsia-600 focus:outline-none"
> >
@ -65,9 +64,17 @@ export function ComposerModal() {
<Dialog.Panel className="relative h-min w-full max-w-xl rounded-lg border border-zinc-800 bg-zinc-900"> <Dialog.Panel className="relative h-min w-full max-w-xl rounded-lg border border-zinc-800 bg-zinc-900">
<div className="flex items-center justify-between px-4 py-4"> <div className="flex items-center justify-between px-4 py-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div>{!isLoading && !isError && account && <User data={account} />}</div> <div>
{!isLoading && !isError && account && (
<User data={account} />
)}
</div>
<span> <span>
<ChevronRightIcon width={14} height={14} className="text-zinc-500" /> <ChevronRightIcon
width={14}
height={14}
className="text-zinc-500"
/>
</span> </span>
<div className="inline-flex h-6 w-max items-center justify-center gap-0.5 rounded bg-zinc-800 pl-3 pr-1.5 text-xs font-medium text-zinc-400 shadow-mini-button"> <div className="inline-flex h-6 w-max items-center justify-center gap-0.5 rounded bg-zinc-800 pl-3 pr-1.5 text-xs font-medium text-zinc-400 shadow-mini-button">
New Post New Post
@ -75,13 +82,19 @@ export function ComposerModal() {
</div> </div>
</div> </div>
<div <div
onClick={closeModal} onKeyDown={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
> >
<CancelIcon width={16} height={16} className="text-zinc-500" /> <CancelIcon
width={16}
height={16}
className="text-zinc-500"
/>
</div> </div>
</div> </div>
{composer.type === 'post' && account && <Post pubkey={account.pubkey} privkey={account.privkey} />} {composer.type === "post" && account && (
<Post pubkey={account.pubkey} privkey={account.privkey} />
)}
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -1,28 +1,38 @@
import { ImageUploader } from '@shared/composer/imageUploader'; import { ImageUploader } from "@shared/composer/imageUploader";
import TrashIcon from '@shared/icons/trash'; import TrashIcon from "@shared/icons/trash";
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from "nostr-tools";
import { useCallback, useContext, useMemo, useState } from 'react'; import { useCallback, useContext, useMemo, useState } from "react";
import { Node, Transforms, createEditor } from 'slate'; import { Node, Transforms, createEditor } from "slate";
import { withHistory } from 'slate-history'; import { withHistory } from "slate-history";
import { Editable, ReactEditor, Slate, useSlateStatic, withReact } from 'slate-react'; import {
Editable,
ReactEditor,
Slate,
useSlateStatic,
withReact,
} from "slate-react";
const withImages = (editor) => { const withImages = (editor) => {
const { isVoid } = editor; const { isVoid } = editor;
editor.isVoid = (element) => { editor.isVoid = (element) => {
return element.type === 'image' ? true : isVoid(element); return element.type === "image" ? true : isVoid(element);
}; };
return editor; return editor;
}; };
const ImagePreview = ({ attributes, children, element }: { attributes: any; children: any; element: any }) => { const ImagePreview = ({
attributes,
children,
element,
}: { attributes: any; children: any; element: any }) => {
const editor: any = useSlateStatic(); const editor: any = useSlateStatic();
const path = ReactEditor.findPath(editor, element); const path = ReactEditor.findPath(editor, element);
@ -30,8 +40,13 @@ const ImagePreview = ({ attributes, children, element }: { attributes: any; chil
<figure {...attributes} className="m-0 mt-3"> <figure {...attributes} className="m-0 mt-3">
{children} {children}
<div contentEditable={false} className="relative"> <div contentEditable={false} className="relative">
<img src={element.url} className="m-0 h-auto w-full rounded-md" /> <img
alt={element.url}
src={element.url}
className="m-0 h-auto w-full rounded-md"
/>
<button <button
type="button"
onClick={() => Transforms.removeNodes(editor, { at: path })} onClick={() => Transforms.removeNodes(editor, { at: path })}
className="absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center gap-0.5 rounded bg-zinc-800 text-xs font-medium text-zinc-400 shadow-mini-button hover:bg-zinc-700" className="absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center gap-0.5 rounded bg-zinc-800 text-xs font-medium text-zinc-400 shadow-mini-button hover:bg-zinc-700"
> >
@ -45,19 +60,22 @@ const ImagePreview = ({ attributes, children, element }: { attributes: any; chil
export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) { export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const editor = useMemo(() => withReact(withImages(withHistory(createEditor()))), []); const editor = useMemo(
() => withReact(withImages(withHistory(createEditor()))),
[],
);
const [content, setContent] = useState<Node[]>([ const [content, setContent] = useState<Node[]>([
{ {
children: [ children: [
{ {
text: '', text: "",
}, },
], ],
}, },
]); ]);
const serialize = useCallback((nodes: Node[]) => { const serialize = useCallback((nodes: Node[]) => {
return nodes.map((n) => Node.string(n)).join('\n'); return nodes.map((n) => Node.string(n)).join("\n");
}, []); }, []);
const submit = () => { const submit = () => {
@ -81,7 +99,7 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const renderElement = useCallback((props: any) => { const renderElement = useCallback((props: any) => {
switch (props.element.type) { switch (props.element.type) {
case 'image': case "image":
if (props.element.url) { if (props.element.url) {
return <ImagePreview {...props} />; return <ImagePreview {...props} />;
} }
@ -95,7 +113,7 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
<div className="flex h-full flex-col px-4 pb-4"> <div className="flex h-full flex-col px-4 pb-4">
<div className="flex h-full w-full gap-2"> <div className="flex h-full w-full gap-2">
<div className="flex w-8 shrink-0 items-center justify-center"> <div className="flex w-8 shrink-0 items-center justify-center">
<div className="h-full w-[2px] bg-zinc-800"></div> <div className="h-full w-[2px] bg-zinc-800" />
</div> </div>
<div className="prose prose-zinc relative h-max w-full max-w-none select-text break-words pb-3 dark:prose-invert prose-p:mb-0.5 prose-p:mt-0 prose-p:text-[15px] prose-p:leading-tight prose-a:text-[15px] prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 prose-a:no-underline hover:prose-a:text-fuchsia-600 hover:prose-a:underline prose-ol:mb-1 prose-ul:mb-1 prose-li:text-[15px] prose-li:leading-tight"> <div className="prose prose-zinc relative h-max w-full max-w-none select-text break-words pb-3 dark:prose-invert prose-p:mb-0.5 prose-p:mt-0 prose-p:text-[15px] prose-p:leading-tight prose-a:text-[15px] prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 prose-a:no-underline hover:prose-a:text-fuchsia-600 hover:prose-a:underline prose-ol:mb-1 prose-ul:mb-1 prose-li:text-[15px] prose-li:leading-tight">
<Editable <Editable
@ -111,7 +129,6 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
<ImageUploader /> <ImageUploader />
<button <button
type="button" type="button"
autoFocus={false}
onClick={submit} onClick={submit}
className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-md bg-fuchsia-500 px-3.5 text-xs font-medium text-zinc-200 shadow-button hover:bg-fuchsia-600" className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-md bg-fuchsia-500 px-3.5 text-xs font-medium text-zinc-200 shadow-button hover:bg-fuchsia-600"
> >

View File

@ -1,6 +1,6 @@
import { Image } from '@shared/image'; import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants'; import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
export function User({ data }: { data: any }) { export function User({ data }: { data: any }) {
const metadata = JSON.parse(data.metadata); const metadata = JSON.parse(data.metadata);
@ -9,7 +9,9 @@ export function User({ data }: { data: any }) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-8 w-8 shrink-0 overflow-hidden rounded bg-zinc-900"> <div className="h-8 w-8 shrink-0 overflow-hidden rounded bg-zinc-900">
<Image <Image
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${metadata?.picture ? metadata.picture : DEFAULT_AVATAR}`} src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${
metadata?.picture ? metadata.picture : DEFAULT_AVATAR
}`}
alt={data.pubkey} alt={data.pubkey}
className="h-8 w-8 object-cover" className="h-8 w-8 object-cover"
loading="auto" loading="auto"
@ -17,7 +19,7 @@ export function User({ data }: { data: any }) {
</div> </div>
<h5 className="text-sm font-semibold leading-none text-zinc-100"> <h5 className="text-sm font-semibold leading-none text-zinc-100">
{metadata?.display_name || metadata?.name || ( {metadata?.display_name || metadata?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700"></div> <div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)} )}
</h5> </h5>
</div> </div>

View File

@ -1,18 +1,18 @@
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from "@shared/relayProvider";
import HeartBeatIcon from '@icons/heartbeat'; import HeartBeatIcon from "@icons/heartbeat";
import { READONLY_RELAYS } from '@stores/constants'; import { READONLY_RELAYS } from "@stores/constants";
import { hasNewerNoteAtom } from '@stores/note'; import { hasNewerNoteAtom } from "@stores/note";
import { dateToUnix } from '@utils/date'; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from '@utils/hooks/useActiveAccount'; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { createChat, createNote, updateAccount } from '@utils/storage'; import { createChat, createNote, updateAccount } from "@utils/storage";
import { getParentID, nip02ToArray } from '@utils/transform'; import { getParentID, nip02ToArray } from "@utils/transform";
import { useSetAtom } from 'jotai'; import { useSetAtom } from "jotai";
import { useContext, useRef } from 'react'; import { useContext, useRef } from "react";
import useSWRSubscription from 'swr/subscription'; import useSWRSubscription from "swr/subscription";
export default function EventCollector() { export default function EventCollector() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
@ -22,7 +22,9 @@ export default function EventCollector() {
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
useSWRSubscription(!isLoading && !isError && account ? ['eventCollector', account] : null, ([, key], {}) => { useSWRSubscription(
!isLoading && !isError && account ? ["eventCollector", account] : null,
([, key]) => {
const follows = JSON.parse(key.follows); const follows = JSON.parse(key.follows);
const followsAsArray = nip02ToArray(follows); const followsAsArray = nip02ToArray(follows);
const unsubscribe = pool.subscribe( const unsubscribe = pool.subscribe(
@ -38,7 +40,7 @@ export default function EventCollector() {
}, },
{ {
kinds: [4], kinds: [4],
'#p': [key.pubkey], "#p": [key.pubkey],
since: dateToUnix(now.current), since: dateToUnix(now.current),
}, },
{ {
@ -51,10 +53,10 @@ export default function EventCollector() {
switch (event.kind) { switch (event.kind) {
// metadata // metadata
case 0: case 0:
updateAccount('metadata', event.content, event.pubkey); updateAccount("metadata", event.content, event.pubkey);
break; break;
// short text note // short text note
case 1: case 1: {
const parentID = getParentID(event.tags, event.id); const parentID = getParentID(event.tags, event.id);
createNote( createNote(
event.id, event.id,
@ -64,15 +66,16 @@ export default function EventCollector() {
event.tags, event.tags,
event.content, event.content,
event.created_at, event.created_at,
parentID parentID,
); );
// notify user reload to get newer note // notify user reload to get newer note
setHasNewerNote(true); setHasNewerNote(true);
break; break;
}
// contacts // contacts
case 3: case 3:
// update account's folllows with NIP-02 tag list // update account's folllows with NIP-02 tag list
updateAccount('follows', event.tags, event.pubkey); updateAccount("follows", event.tags, event.pubkey);
break; break;
// chat // chat
case 4: case 4:
@ -90,24 +93,34 @@ export default function EventCollector() {
event.tags, event.tags,
event.content, event.content,
event.created_at, event.created_at,
event.id event.id,
); );
break; break;
// long post // long post
case 30023: case 30023:
// insert event to local database // insert event to local database
createNote(event.id, account.id, event.pubkey, event.kind, event.tags, event.content, event.created_at, ''); createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
"",
);
break; break;
default: default:
break; break;
} }
} },
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}); },
);
return ( return (
<div className="inline-flex h-6 w-6 items-center justify-center rounded text-zinc-400 hover:bg-zinc-900 hover:text-zinc-200"> <div className="inline-flex h-6 w-6 items-center justify-center rounded text-zinc-400 hover:bg-zinc-900 hover:text-zinc-200">

View File

@ -1,31 +1,31 @@
import PlusIcon from '@icons/plus'; import PlusIcon from "@icons/plus";
import { channelContentAtom } from '@stores/channel'; import { channelContentAtom } from "@stores/channel";
import { chatContentAtom } from '@stores/chat'; import { chatContentAtom } from "@stores/chat";
import { noteContentAtom } from '@stores/note'; import { noteContentAtom } from "@stores/note";
import { createBlobFromFile } from '@utils/createBlobFromFile'; import { createBlobFromFile } from "@utils/createBlobFromFile";
import { open } from '@tauri-apps/api/dialog'; import { open } from "@tauri-apps/api/dialog";
import { Body, fetch } from '@tauri-apps/api/http'; import { Body, fetch } from "@tauri-apps/api/http";
import { useSetAtom } from 'jotai'; import { useSetAtom } from "jotai";
import { useState } from 'react'; import { useState } from "react";
export function ImagePicker({ type }: { type: string }) { export function ImagePicker({ type }: { type: string }) {
let atom; let atom;
switch (type) { switch (type) {
case 'note': case "note":
atom = noteContentAtom; atom = noteContentAtom;
break; break;
case 'chat': case "chat":
atom = chatContentAtom; atom = chatContentAtom;
break; break;
case 'channel': case "channel":
atom = channelContentAtom; atom = channelContentAtom;
break; break;
default: default:
throw new Error('Invalid type'); throw new Error("Invalid type");
} }
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -36,8 +36,8 @@ export function ImagePicker({ type }: { type: string }) {
multiple: false, multiple: false,
filters: [ filters: [
{ {
name: 'Image', name: "Image",
extensions: ['png', 'jpeg', 'jpg', 'gif'], extensions: ["png", "jpeg", "jpg", "gif"],
}, },
], ],
}); });
@ -48,31 +48,35 @@ export function ImagePicker({ type }: { type: string }) {
} else { } else {
setLoading(true); setLoading(true);
const filename = selected.split('/').pop(); const filename = selected.split("/").pop();
const file = await createBlobFromFile(selected); const file = await createBlobFromFile(selected);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
const res: { data: { file: { id: string } } } = await fetch('https://void.cat/upload?cli=false', { const res: { data: { file: { id: string } } } = await fetch(
method: 'POST', "https://void.cat/upload?cli=false",
{
method: "POST",
timeout: 5, timeout: 5,
headers: { headers: {
accept: '*/*', accept: "*/*",
'Content-Type': 'application/octet-stream', "Content-Type": "application/octet-stream",
'V-Filename': filename, "V-Filename": filename,
'V-Description': 'Upload from https://lume.nu', "V-Description": "Upload from https://lume.nu",
'V-Strip-Metadata': 'true', "V-Strip-Metadata": "true",
}, },
body: Body.bytes(buf), body: Body.bytes(buf),
}); },
const webpImage = 'https://void.cat/d/' + res.data.file.id + '.webp'; );
const webpImage = `https://void.cat/d/${res.data.file.id}.webp`;
setValue((content: string) => content + ' ' + webpImage); setValue((content: string) => `${content} ${webpImage}`);
setLoading(false); setLoading(false);
} }
}; };
return ( return (
<button <button
type="button"
onClick={() => openFileDialog()} onClick={() => openFileDialog()}
className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-700" className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-700"
> >
@ -83,12 +87,20 @@ export function ImagePicker({ type }: { type: string }) {
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> />
</svg> </svg>
) : ( ) : (
<PlusIcon width={16} height={16} className="text-zinc-400" /> <PlusIcon width={16} height={16} className="text-zinc-400" />

View File

@ -1,8 +1,15 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export default function ArrowLeftIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export default function ArrowLeftIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M10 18.25L3.75 12M3.75 12L10 5.75M3.75 12H20.25" d="M10 18.25L3.75 12M3.75 12L10 5.75M3.75 12H20.25"
stroke="currentColor" stroke="currentColor"

View File

@ -1,8 +1,15 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export default function ArrowRightIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export default function ArrowRightIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M14 5.75L20.25 12M20.25 12L14 18.25M20.25 12H3.75" d="M14 5.75L20.25 12M20.25 12L14 18.25M20.25 12H3.75"
stroke="currentColor" stroke="currentColor"

View File

@ -1,8 +1,17 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export default function BellIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export default function BellIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M16 18.25C15.3267 20.0159 13.7891 21.25 12 21.25C10.2109 21.25 8.67327 20.0159 8 18.25M20.5 18.25L18.9554 8.67345C18.4048 5.2596 15.458 2.75 12 2.75C8.54203 2.75 5.59523 5.2596 5.04461 8.67345L3.5 18.25H20.5Z" d="M16 18.25C15.3267 20.0159 13.7891 21.25 12 21.25C10.2109 21.25 8.67327 20.0159 8 18.25M20.5 18.25L18.9554 8.67345C18.4048 5.2596 15.458 2.75 12 2.75C8.54203 2.75 5.59523 5.2596 5.04461 8.67345L3.5 18.25H20.5Z"
stroke="currentColor" stroke="currentColor"

View File

@ -1,8 +1,15 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export default function CancelIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export default function CancelIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M4.75 4.75L19.25 19.25M19.25 4.75L4.75 19.25" d="M4.75 4.75L19.25 19.25M19.25 4.75L4.75 19.25"
stroke="currentColor" stroke="currentColor"

View File

@ -1,8 +1,17 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export default function CheckCircleIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export default function CheckCircleIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
fillRule="evenodd" fillRule="evenodd"
clipRule="evenodd" clipRule="evenodd"

View File

@ -1,8 +1,17 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export default function ChevronDownIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export default function ChevronDownIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M8 10L12 14L16 10" d="M8 10L12 14L16 10"
stroke="currentColor" stroke="currentColor"

Some files were not shown because too many files have changed in this diff Show More