mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 11:43:30 +00:00
replace eslint/prettier with rome
This commit is contained in:
parent
48d690d33a
commit
409a625dcc
@ -1,8 +0,0 @@
|
|||||||
.git/
|
|
||||||
|
|
||||||
.vscode-test/
|
|
||||||
out/
|
|
||||||
dist/
|
|
||||||
test-fixtures/
|
|
||||||
node_modules/
|
|
||||||
src-tauri/
|
|
24
.prettierrc
24
.prettierrc
@ -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
|
|
||||||
}
|
|
14
package.json
14
package.json
@ -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",
|
||||||
|
3264
pnpm-lock.yaml
3264
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
18
rome.json
Normal file
18
rome.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
export { LayoutOnboarding as Layout } from './layout';
|
export { LayoutOnboarding as Layout } from "./layout";
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -1 +1 @@
|
|||||||
export { LayoutChannel as Layout } from './layout';
|
export { LayoutChannel as Layout } from "./layout";
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
@ -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"
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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} />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
@ -1 +1 @@
|
|||||||
export { LayoutChat as Layout } from './layout';
|
export { LayoutChat as Layout } from "./layout";
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 (
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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 () => {
|
||||||
|
@ -1 +1 @@
|
|||||||
export const filesystemRoutingRoot = '/';
|
export const filesystemRoutingRoot = "/";
|
||||||
|
@ -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" />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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's an interesting fact:
|
Here'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>
|
||||||
|
@ -1 +1 @@
|
|||||||
export { LayoutNewsfeed as Layout } from './layout';
|
export { LayoutNewsfeed as Layout } from "./layout";
|
||||||
|
@ -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} />
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
@ -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} />
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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} />
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
export { LayoutNewsfeed as Layout } from './layout';
|
export { LayoutNewsfeed as Layout } from "./layout";
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 };
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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" />
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user