replace eslint/prettier with rome

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

18
rome.json Normal file
View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
import ArrowLeftIcon from '@icons/arrowLeft';
import ArrowRightIcon from '@icons/arrowRight';
import ArrowLeftIcon from "@icons/arrowLeft";
import ArrowRightIcon from "@icons/arrowRight";
import useSWR from 'swr';
import useSWR from "swr";
const fetcher = async () => {
const { platform } = await import('@tauri-apps/api/os');
const { platform } = await import("@tauri-apps/api/os");
return await platform();
};
export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
const { data: platform } = useSWR('platform', fetcher);
const { data: platform } = useSWR("platform", fetcher);
const goBack = () => {
window.history.back();
@ -26,19 +26,36 @@ export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
data-tauri-drag-region
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 className={`flex h-full items-center gap-2 ${platform === 'darwin' ? 'pl-[68px]' : ''}`}>
<div
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
type="button"
onClick={() => goBack()}
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
type="button"
onClick={() => goForward()}
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>
</div>
</div>

View File

@ -1,15 +1,15 @@
import EyeOffIcon from '@icons/eyeOff';
import EyeOnIcon from '@icons/eyeOn';
import EyeOffIcon from "@icons/eyeOff";
import EyeOnIcon from "@icons/eyeOn";
import { onboardingAtom } from '@stores/onboarding';
import { onboardingAtom } from "@stores/onboarding";
import { useSetAtom } from 'jotai';
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
import { useMemo, useState } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
import { useSetAtom } from "jotai";
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
import { useMemo, useState } from "react";
import { navigate } from "vite-plugin-ssr/client/router";
export function Page() {
const [type, setType] = useState('password');
const [type, setType] = useState("password");
const setOnboarding = useSetAtom(onboardingAtom);
const privkey = useMemo(() => generatePrivateKey(), []);
@ -19,27 +19,31 @@ export function Page() {
// toggle private key
const showPrivateKey = () => {
if (type === 'password') {
setType('text');
if (type === "password") {
setType("text");
} else {
setType('password');
setType("password");
}
};
const submit = () => {
setOnboarding((prev) => ({ ...prev, pubkey: pubkey, privkey: privkey }));
navigate('/auth/create/step-2');
navigate("/auth/create/step-2");
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
<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 className="flex flex-col gap-4">
<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">
<input
readOnly
@ -49,7 +53,9 @@ export function Page() {
</div>
</div>
<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">
<input
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"
/>
<button
type="button"
onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{type === 'password' ? (
<EyeOffIcon width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
{type === "password" ? (
<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>
</div>

View File

@ -1,13 +1,13 @@
import { AvatarUploader } from '@shared/avatarUploader';
import { Image } from '@shared/image';
import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants';
import { onboardingAtom } from '@stores/onboarding';
import { DEFAULT_AVATAR } from "@stores/constants";
import { onboardingAtom } from "@stores/onboarding";
import { useAtom } from 'jotai';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { navigate } from 'vite-plugin-ssr/client/router';
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { navigate } from "vite-plugin-ssr/client/router";
export function Page() {
const [image, setImage] = useState(DEFAULT_AVATAR);
@ -32,52 +32,70 @@ export function Page() {
const onSubmit = (data: any) => {
setLoading(true);
setOnboarding((prev) => ({ ...prev, metadata: data }));
navigate('/auth/create/step-3');
navigate("/auth/create/step-3");
};
useEffect(() => {
setValue('picture', image);
setValue("picture", image);
}, [setValue, image]);
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
<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 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
type={'hidden'}
{...register('picture')}
type={"hidden"}
{...register("picture")}
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"
/>
<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">
<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">
<AvatarUploader valueState={setImage} />
</div>
</div>
</div>
<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">
<input
type={'text'}
{...register('display_name', { required: true, minLength: 4 })}
type={"text"}
{...register("display_name", {
required: true,
minLength: 4,
})}
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"
/>
</div>
</div>
<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">
<textarea
{...register('about')}
{...register("about")}
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"
/>
@ -96,6 +114,7 @@ export function Page() {
fill="none"
viewBox="0 0 24 24"
>
<title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
@ -103,12 +122,12 @@ export function Page() {
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
/>
</svg>
) : (
<span>Continue </span>

View File

@ -1,53 +1,117 @@
import User from '@app/auth/components/user';
import User from "@app/auth/components/user";
import { RelayContext } from '@shared/relayProvider';
import { RelayContext } from "@shared/relayProvider";
import CheckCircleIcon from '@icons/checkCircle';
import CheckCircleIcon from "@icons/checkCircle";
import { WRITEONLY_RELAYS } from '@stores/constants';
import { onboardingAtom } from '@stores/onboarding';
import { WRITEONLY_RELAYS } from "@stores/constants";
import { onboardingAtom } from "@stores/onboarding";
import { createAccount, createPleb } from '@utils/storage';
import { arrayToNIP02 } from '@utils/transform';
import { createAccount, createPleb } from "@utils/storage";
import { arrayToNIP02 } from "@utils/transform";
import { useAtom } from 'jotai';
import { getEventHash, signEvent } from 'nostr-tools';
import { useContext, useState } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
import { useAtom } from "jotai";
import { getEventHash, signEvent } from "nostr-tools";
import { useContext, useState } from "react";
import { navigate } from "vite-plugin-ssr/client/router";
const initialList = [
{ pubkey: '82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2' },
{ pubkey: 'a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98' },
{ pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9' },
{ pubkey: 'c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0' },
{ pubkey: '6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93' },
{ pubkey: 'e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411' },
{ pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d' },
{ pubkey: 'c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15' },
{ pubkey: 'e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42' },
{ pubkey: '84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240' },
{ 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' },
{
pubkey: "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
},
{
pubkey: "a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98",
},
{
pubkey: "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
},
{
pubkey: "c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0",
},
{
pubkey: "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
},
{
pubkey: "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411",
},
{
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
},
{
pubkey: "c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15",
},
{
pubkey: "e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42",
},
{
pubkey: "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
},
{
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() {
@ -59,7 +123,9 @@ export function Page() {
// toggle follow state
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);
};
@ -82,7 +148,7 @@ export function Page() {
const nip02 = arrayToNIP02(follows);
// build event
const event: any = {
content: '',
content: "",
created_at: Math.floor(Date.now() / 1000),
kind: 3,
pubkey: onboarding.pubkey,
@ -100,17 +166,26 @@ export function Page() {
const followsIncludeSelf = follows.concat([onboarding.pubkey]);
// 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) => {
if (res) {
for (const tag of follows) {
fetch(`https://us.rbr.bio/${tag}/metadata.json`)
.then((data) => data.json())
.then((data) => createPleb(tag, data ?? ''));
.then((data) => createPleb(tag, data ?? ""));
}
broadcastAccount();
broadcastContacts();
setTimeout(() => navigate('/', { overwriteLastHistoryEntry: true }), 2000);
setTimeout(
() => navigate("/", { overwriteLastHistoryEntry: true }),
2000,
);
} else {
console.error();
}
@ -122,7 +197,9 @@ export function Page() {
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
<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 className="flex flex-col gap-4">
<div className="w-full rounded-lg border border-zinc-800 bg-zinc-900">
@ -130,13 +207,13 @@ export function Page() {
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">
{follows.length}/10
</span>{' '}
</span>{" "}
plebs
</div>
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
{initialList.map((item: { pubkey: string }, index: number) => (
<button
key={index}
key={`item-${index}`}
type="button"
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"
@ -144,7 +221,11 @@ export function Page() {
<User pubkey={item.pubkey} />
{follows.includes(item.pubkey) && (
<div>
<CheckCircleIcon width={16} height={16} className="text-green-400" />
<CheckCircleIcon
width={16}
height={16}
className="text-green-400"
/>
</div>
)}
</button>
@ -153,6 +234,7 @@ export function Page() {
</div>
{follows.length >= 10 && (
<button
type="button"
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"
>
@ -163,12 +245,20 @@ export function Page() {
fill="none"
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
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
/>
</svg>
) : (
<span>Continue </span>

View File

@ -1,9 +1,9 @@
import { onboardingAtom } from '@stores/onboarding';
import { onboardingAtom } from "@stores/onboarding";
import { useSetAtom } from 'jotai';
import { getPublicKey, nip19 } from 'nostr-tools';
import { Resolver, useForm } from 'react-hook-form';
import { navigate } from 'vite-plugin-ssr/client/router';
import { useSetAtom } from "jotai";
import { getPublicKey, nip19 } from "nostr-tools";
import { Resolver, useForm } from "react-hook-form";
import { navigate } from "vite-plugin-ssr/client/router";
type FormValues = {
key: string;
@ -15,8 +15,8 @@ const resolver: Resolver<FormValues> = async (values) => {
errors: !values.key
? {
key: {
type: 'required',
message: 'This is required.',
type: "required",
message: "This is required.",
},
}
: {},
@ -35,20 +35,20 @@ export function Page() {
const onSubmit = async (data: any) => {
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;
}
if (typeof getPublicKey(privkey) === 'string') {
if (typeof getPublicKey(privkey) === "string") {
setOnboardingPrivkey((prev) => ({ ...prev, privkey: privkey }));
navigate(`/auth/import/step-2`);
navigate("/auth/import/step-2");
}
} catch (error) {
setError('key', {
type: 'custom',
message: 'Private Key is invalid, please check again',
setError("key", {
type: "custom",
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="mx-auto w-full max-w-md">
<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 className="flex flex-col gap-4">
<div>
{/* #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">
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
Coming soon
@ -73,23 +78,28 @@ export function Page() {
</div>
<div className="relative">
<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 className="relative flex justify-center">
<span className="bg-zinc-950 px-2 text-sm text-zinc-500">or</span>
</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="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
{...register('key', { required: true, minLength: 32 })}
type={'password'}
{...register("key", { required: true, minLength: 32 })}
type={"password"}
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"
/>
</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 className="flex h-9 items-center justify-center">
{isSubmitting ? (
@ -99,12 +109,20 @@ export function Page() {
fill="none"
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
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
/>
</svg>
) : (
<button

View File

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

View File

@ -1,48 +1,48 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import ArrowRightIcon from '@icons/arrowRight';
import ArrowRightIcon from "@icons/arrowRight";
const PLEBS = [
'https://133332.xyz/p.jpg',
'https://void.cat/d/3Bp6jSHURFNQ9u3pK8nwtq.webp',
'https://i.imgur.com/f8SyhRL.jpg',
'http://nostr.build/i/6369.jpg',
'https://pbs.twimg.com/profile_images/1622010345589190656/mAPqsmtz_400x400.jpg',
'https://media.tenor.com/l5arkXy9RfIAAAAd/thunder.gif',
'https://nostr.build/i/p/nostr.build_0e412058980ed2ac4adf3de639304c9e970e2745ba9ca19c75f984f4f6da4971.jpeg',
'https://nostr.build/i/nostr.build_864a019a6c1d3a90a17363553d32b71de618d250f02cf0a59ca19fb3029fd5bc.jpg',
'https://void.cat/d/8zE9T8a39YfUVjrLM4xcpE.webp',
'https://avatars.githubusercontent.com/u/89577423',
'https://pbs.twimg.com/profile_images/1363180486080663554/iN-r_BiM_400x400.jpg',
'https://void.cat/d/JUBBqXgCcGBEh7jUgJaayy',
'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://void.cat/d/KvAEMvYNmy1rfCH6a7HZzh.webp',
'https://media.giphy.com/media/NqfMNCkyGwtXhKFlCR/giphy-downsized-large.gif',
'https://i.imgur.com/VGpUNFS.jpg',
'https://nostr.build/i/p/nostr.build_b39254db43d5557df99d1eb516f1c2f56a21a01b10c248f6eb66aa827c9a90f4.jpeg',
'https://davidcoen.it/wp-content/uploads/2020/11/7004972-taglio.jpg',
'https://pbs.twimg.com/profile_images/1570432066348515330/26PtCuwF_400x400.jpg',
'https://nostr.build/i/nostr.build_9d33ee801aa08955be174554832952ab95a65d5e015176834c8aa9a4e2f2e3a5.jpg',
'https://www.linkpicture.com/q/0FE78CFF-C931-4568-A7AA-DD8AEE889992.jpeg',
'https://nostr.build/i/nostr.build_97d6e2d25dd92422eb3d6d645b7cee9ed9c614f331be7e6f7db9ccfdbc5ee260.png',
'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/1639659216372658178/Dnn-Ysp-_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/1638644441773748226/tNsA6RpG_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/1523971278478131200/TMPzfvhE_400x400.jpg',
'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/1409612480465276931/38Vyx4e8_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/1548660003522887682/1QMHmles_400x400.jpg',
'https://pbs.twimg.com/profile_images/1362497143999787013/KLUoN1Vn_400x400.png',
'https://pbs.twimg.com/profile_images/1600434913240563713/AssmMGwf_400x400.jpg',
"https://133332.xyz/p.jpg",
"https://void.cat/d/3Bp6jSHURFNQ9u3pK8nwtq.webp",
"https://i.imgur.com/f8SyhRL.jpg",
"http://nostr.build/i/6369.jpg",
"https://pbs.twimg.com/profile_images/1622010345589190656/mAPqsmtz_400x400.jpg",
"https://media.tenor.com/l5arkXy9RfIAAAAd/thunder.gif",
"https://nostr.build/i/p/nostr.build_0e412058980ed2ac4adf3de639304c9e970e2745ba9ca19c75f984f4f6da4971.jpeg",
"https://nostr.build/i/nostr.build_864a019a6c1d3a90a17363553d32b71de618d250f02cf0a59ca19fb3029fd5bc.jpg",
"https://void.cat/d/8zE9T8a39YfUVjrLM4xcpE.webp",
"https://avatars.githubusercontent.com/u/89577423",
"https://pbs.twimg.com/profile_images/1363180486080663554/iN-r_BiM_400x400.jpg",
"https://void.cat/d/JUBBqXgCcGBEh7jUgJaayy",
"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://void.cat/d/KvAEMvYNmy1rfCH6a7HZzh.webp",
"https://media.giphy.com/media/NqfMNCkyGwtXhKFlCR/giphy-downsized-large.gif",
"https://i.imgur.com/VGpUNFS.jpg",
"https://nostr.build/i/p/nostr.build_b39254db43d5557df99d1eb516f1c2f56a21a01b10c248f6eb66aa827c9a90f4.jpeg",
"https://davidcoen.it/wp-content/uploads/2020/11/7004972-taglio.jpg",
"https://pbs.twimg.com/profile_images/1570432066348515330/26PtCuwF_400x400.jpg",
"https://nostr.build/i/nostr.build_9d33ee801aa08955be174554832952ab95a65d5e015176834c8aa9a4e2f2e3a5.jpg",
"https://www.linkpicture.com/q/0FE78CFF-C931-4568-A7AA-DD8AEE889992.jpeg",
"https://nostr.build/i/nostr.build_97d6e2d25dd92422eb3d6d645b7cee9ed9c614f331be7e6f7db9ccfdbc5ee260.png",
"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/1639659216372658178/Dnn-Ysp-_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/1638644441773748226/tNsA6RpG_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/1523971278478131200/TMPzfvhE_400x400.jpg",
"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/1409612480465276931/38Vyx4e8_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/1548660003522887682/1QMHmles_400x400.jpg",
"https://pbs.twimg.com/profile_images/1362497143999787013/KLUoN1Vn_400x400.png",
"https://pbs.twimg.com/profile_images/1600434913240563713/AssmMGwf_400x400.jpg",
];
const DURATION = 50000;
@ -52,17 +52,21 @@ const PLEBS_PER_ROW = 20;
const random = (min, max) => Math.floor(Math.random() * (max - min)) + min;
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 (
<div>
<div
className="flex w-fit"
style={{
animationName: 'loop',
animationIterationCount: 'infinite',
animationDirection: reverse ? 'reverse' : 'normal',
animationDuration: duration + 'ms',
animationTimingFunction: 'linear',
animationName: "loop",
animationIterationCount: "infinite",
animationDirection: reverse ? "reverse" : "normal",
animationDuration: `${duration}ms`,
animationTimingFunction: "linear",
}}
>
{children}
@ -78,12 +82,23 @@ export function Page() {
<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">
{[...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)
.slice(0, PLEBS_PER_ROW)
.map((tag) => (
<div 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
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>
))}
</InfiniteLoopSlider>

View File

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

View File

@ -1,9 +1,9 @@
import MutedItem from '@app/channel/components/mutedItem';
import MutedItem from "@app/channel/components/mutedItem";
import MuteIcon from '@icons/mute';
import MuteIcon from "@icons/mute";
import { Popover, Transition } from '@headlessui/react';
import { Fragment } from 'react';
import { Popover, Transition } from "@headlessui/react";
import { Fragment } from "react";
export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
return (
@ -12,10 +12,16 @@ export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
<>
<Popover.Button
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>
<Transition
as={Fragment}
@ -34,7 +40,8 @@ export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
Your muted list
</h3>
<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>
</div>
</div>

View File

@ -1,22 +1,22 @@
import { AvatarUploader } from '@shared/avatarUploader';
import { Image } from '@shared/image';
import { RelayContext } from '@shared/relayProvider';
import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import CancelIcon from '@icons/cancel';
import PlusIcon from '@icons/plus';
import CancelIcon from "@icons/cancel";
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 { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { createChannel } from '@utils/storage';
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { createChannel } from "@utils/storage";
import { Dialog, Transition } from '@headlessui/react';
import { getEventHash, signEvent } from 'nostr-tools';
import { Fragment, useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useSWRConfig } from 'swr';
import { navigate } from 'vite-plugin-ssr/client/router';
import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, signEvent } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useSWRConfig } from "swr";
import { navigate } from "vite-plugin-ssr/client/router";
export default function ChannelCreateModal() {
const pool: any = useContext(RelayContext);
@ -63,7 +63,7 @@ export default function ChannelCreateModal() {
// insert to database
createChannel(event.id, event.pubkey, event.content, event.created_at);
// update channe llist
mutate('channels');
mutate("channels");
// reset form
reset();
setTimeout(() => {
@ -73,12 +73,12 @@ export default function ChannelCreateModal() {
navigate(`/app/channel?id=${event.id}`);
}, 2000);
} else {
console.log('error');
console.log("error");
}
};
useEffect(() => {
setValue('picture', image);
setValue("picture", image);
}, [setValue, image]);
return (
@ -92,7 +92,9 @@ export default function ChannelCreateModal() {
<PlusIcon width={12} height={12} className="text-zinc-500" />
</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>
</button>
<Transition appear show={isOpen} as={Fragment}>
@ -131,30 +133,42 @@ export default function ChannelCreateModal() {
<button
type="button"
onClick={closeModal}
autoFocus={false}
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>
</div>
<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
speech
Channels are freedom square, everyone can speech freely,
no one can stop you or deceive what to speech
</Dialog.Description>
</div>
</div>
<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
type={'hidden'}
{...register('picture')}
type={"hidden"}
{...register("picture")}
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"
/>
<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">
<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">
<AvatarUploader valueState={setImage} />
</div>
@ -166,8 +180,11 @@ export default function ChannelCreateModal() {
</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">
<input
type={'text'}
{...register('name', { required: true, minLength: 4 })}
type={"text"}
{...register("name", {
required: true,
minLength: 4,
})}
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"
/>
@ -179,7 +196,7 @@ export default function ChannelCreateModal() {
</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">
<textarea
{...register('about')}
{...register("about")}
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"
/>
@ -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 flex-col gap-0.5">
<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">
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
Coming soon
@ -201,12 +220,13 @@ export default function ChannelCreateModal() {
</div>
<div>
<button
type="button"
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"
role="switch"
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>
</div>
</div>
@ -223,6 +243,7 @@ export default function ChannelCreateModal() {
fill="none"
viewBox="0 0 24 24"
>
<title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
@ -230,15 +251,15 @@ export default function ChannelCreateModal() {
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
/>
</svg>
) : (
'Create channel'
"Create channel"
)}
</button>
</div>

View File

@ -1,8 +1,8 @@
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile';
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
import { usePageContext } from '@utils/hooks/usePageContext';
import { usePageContext } from "@utils/hooks/usePageContext";
import { twMerge } from 'tailwind-merge';
import { twMerge } from "tailwind-merge";
export default function ChannelsListItem({ data }: { data: any }) {
const channel: any = useChannelProfile(data.event_id, data.pubkey);
@ -15,20 +15,26 @@ export default function ChannelsListItem({ data }: { data: any }) {
<a
href={`/app/channel?id=${data.event_id}&channelpub=${data.pubkey}`}
className={twMerge(
'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' : ''
"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"
: "",
)}
>
<div
className={twMerge(
'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' : ''
"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"
: "",
)}
>
<span className="text-xs text-zinc-200">#</span>
</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>
</a>
);

View File

@ -1,30 +1,32 @@
import ChannelCreateModal from '@app/channel/components/createModal';
import ChannelsListItem from '@app/channel/components/item';
import ChannelCreateModal from "@app/channel/components/createModal";
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);
export default function ChannelsList() {
const { data, error }: any = useSWR('channels', fetcher);
const { data, error }: any = useSWR("channels", fetcher);
return (
<div className="flex flex-col gap-px">
{!data || error ? (
<>
<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="h-3 w-full animate-pulse rounded-sm 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="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="h-3 w-full animate-pulse rounded-sm 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>
</>
) : (
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 />
</div>

View File

@ -1,8 +1,8 @@
import MiniMember from '@app/channel/components/miniMember';
import MiniMember from "@app/channel/components/miniMember";
import { channelMembersAtom } from '@stores/channel';
import { channelMembersAtom } from "@stores/channel";
import { useAtomValue } from 'jotai';
import { useAtomValue } from "jotai";
export default function ChannelMembers() {
const membersAsSet = useAtomValue(channelMembersAtom);
@ -10,26 +10,30 @@ export default function ChannelMembers() {
const miniMembersList = membersAsArray.slice(0, 4);
const totalMembers =
membersAsArray.length > 0
? '+' +
Intl.NumberFormat('en-US', {
notation: 'compact',
? `+${Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
}).format(membersAsArray.length)
}).format(membersAsArray.length)}`
: 0;
return (
<div>
<div className="group flex -space-x-2 overflow-hidden hover:-space-x-1">
{miniMembersList.map((member, index) => (
<MiniMember key={index} pubkey={member} />
<MiniMember key={`item-${index}`} pubkey={member} />
))}
{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">
<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>
<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
</button>
</div>

View File

@ -1,12 +1,12 @@
import ChannelMessageItem from '@app/channel/components/messages/item';
import ChannelMessageItem from "@app/channel/components/messages/item";
import { sortedChannelMessagesAtom } from '@stores/channel';
import { sortedChannelMessagesAtom } from "@stores/channel";
import { getHourAgo } from '@utils/date';
import { getHourAgo } from "@utils/date";
import { useAtomValue } from 'jotai';
import { useCallback, useRef } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { useAtomValue } from "jotai";
import { useCallback, useRef } from "react";
import { Virtuoso } from "react-virtuoso";
export default function ChannelMessageList() {
const now = useRef(new Date());
@ -17,14 +17,14 @@ export default function ChannelMessageList() {
(index: string | number) => {
return <ChannelMessageItem data={data[index]} />;
},
[data]
[data],
);
const computeItemKey = useCallback(
(index: string | number) => {
return data[index].id;
},
[data]
[data],
);
return (
@ -36,16 +36,19 @@ export default function ChannelMessageList() {
components={{
Header: () => (
<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>
<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">
{getHourAgo(24, now.current).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
{getHourAgo(24, now.current).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</div>
</div>
@ -53,8 +56,12 @@ export default function ChannelMessageList() {
),
EmptyPlaceholder: () => (
<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>
<p className="text-sm leading-none text-zinc-400">Be the first to share a message in this channel.</p>
<h3 className="text-sm font-semibold leading-none text-zinc-200">
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>
),
}}

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import MessageHideButton from '@app/channel/components/messages/hideButton';
import MessageMuteButton from '@app/channel/components/messages/muteButton';
import MessageReplyButton from '@app/channel/components/messages/replyButton';
import ChannelMessageUser from '@app/channel/components/messages/user';
import MessageHideButton from "@app/channel/components/messages/hideButton";
import MessageMuteButton from "@app/channel/components/messages/muteButton";
import MessageReplyButton from "@app/channel/components/messages/replyButton";
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 }) {
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="flex flex-col gap-2">
<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 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">
<MessageReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
<MessageReplyButton
id={data.id}
pubkey={data.pubkey}
content={data.content}
/>
<MessageHideButton id={data.id} />
<MessageMuteButton pubkey={data.pubkey} />
</div>

View File

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

View File

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

View File

@ -1,26 +1,29 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants';
import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/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);
return (
<div className="group flex items-start gap-3">
{isError || isLoading ? (
<>
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div>
<div className="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 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>
</>
@ -28,7 +31,9 @@ export default function ChannelMessageUser({ pubkey, time }: { pubkey: string; t
<>
<div className="relative h-9 w-9 shrink rounded-md">
<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}
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)}
</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>
</>

View File

@ -1,9 +1,9 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants';
import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
export default function UserReply({ pubkey }: { pubkey: string }) {
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">
{isError || isLoading ? (
<>
<div className="relative h-7 w-7 shrink animate-pulse overflow-hidden rounded bg-zinc-800"></div>
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-xs font-medium leading-none text-zinc-500"></span>
<div className="relative h-7 w-7 shrink 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" />
</>
) : (
<>
<div className="relative h-7 w-7 shrink overflow-hidden rounded">
<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}
className="h-7 w-7 rounded object-cover"
/>

View File

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

View File

@ -1,8 +1,8 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile';
import { useProfile } from "@utils/hooks/useProfile";
export default function MiniMember({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey);
@ -10,12 +10,12 @@ export default function MiniMember({ pubkey }: { pubkey: string }) {
return (
<>
{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
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}
alt={user?.pubkey || 'user avatar'}
alt={user?.pubkey || "user avatar"}
/>
)}
</>

View File

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

View File

@ -1,20 +1,20 @@
import { AvatarUploader } from '@shared/avatarUploader';
import { Image } from '@shared/image';
import { RelayContext } from '@shared/relayProvider';
import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import CancelIcon from '@icons/cancel';
import EditIcon from '@icons/edit';
import CancelIcon from "@icons/cancel";
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 { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { getChannel } from '@utils/storage';
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getChannel } from "@utils/storage";
import { Dialog, Transition } from '@headlessui/react';
import { getEventHash, signEvent } from 'nostr-tools';
import { Fragment, useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, signEvent } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
export default function ChannelUpdateModal({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
@ -58,7 +58,7 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
created_at: dateToUnix(),
kind: 41,
pubkey: account.pubkey,
tags: [['e', id]],
tags: [["e", id]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
@ -71,12 +71,12 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
setIsOpen(false);
setLoading(false);
} else {
console.log('error');
console.log("error");
}
};
useEffect(() => {
setValue('picture', image);
setValue("picture", image);
}, [setValue, image]);
return (
@ -86,7 +86,11 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
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"
>
<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>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
@ -124,30 +128,42 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
<button
type="button"
onClick={closeModal}
autoFocus={false}
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>
</div>
<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
please carefully.
New metadata will be published on all relays, and will be
immediately available to all users, so please carefully.
</Dialog.Description>
</div>
</div>
<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
type={'hidden'}
{...register('picture')}
type={"hidden"}
{...register("picture")}
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"
/>
<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">
<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">
<AvatarUploader valueState={setImage} />
</div>
@ -159,8 +175,11 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
</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">
<input
type={'text'}
{...register('name', { required: true, minLength: 4 })}
type={"text"}
{...register("name", {
required: true,
minLength: 4,
})}
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"
/>
@ -172,7 +191,7 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
</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">
<textarea
{...register('about')}
{...register("about")}
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"
/>
@ -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 flex-col gap-0.5">
<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">
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
Coming soon
@ -194,12 +215,13 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
</div>
<div>
<button
type="button"
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"
role="switch"
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>
</div>
</div>
@ -216,6 +238,7 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
fill="none"
viewBox="0 0 24 24"
>
<title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
@ -223,15 +246,15 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
/>
</svg>
) : (
'Update channel'
"Update channel"
)}
</button>
</div>

View File

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

View File

@ -1,6 +1,6 @@
import AppHeader from '@shared/appHeader';
import MultiAccounts from '@shared/multiAccounts';
import Navigation from '@shared/navigation';
import AppHeader from "@shared/appHeader";
import MultiAccounts from "@shared/multiAccounts";
import Navigation from "@shared/navigation";
export function LayoutChannel({ children }: { children: React.ReactNode }) {
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">
<Navigation />
</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>

View File

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

View File

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

View File

@ -1,12 +1,12 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants';
import { DEFAULT_AVATAR } from "@stores/constants";
import { usePageContext } from '@utils/hooks/usePageContext';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { usePageContext } from "@utils/hooks/usePageContext";
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import { twMerge } from 'tailwind-merge';
import { twMerge } from "tailwind-merge";
export default function ChatsListItem({ pubkey }: { pubkey: string }) {
const pageContext = usePageContext();
@ -21,17 +21,19 @@ export default function ChatsListItem({ pubkey }: { pubkey: string }) {
{isError && <div>error</div>}
{isLoading && !user ? (
<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 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>
) : (
<a
href={`/app/chat?pubkey=${pubkey}`}
className={twMerge(
'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' : ''
"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"
: "",
)}
>
<div className="relative h-5 w-5 shrink-0 rounded">

View File

@ -1,16 +1,19 @@
import ChatsListItem from '@app/chat/components/item';
import ChatsListSelfItem from '@app/chat/components/self';
import ChatsListItem from "@app/chat/components/item";
import ChatsListSelfItem from "@app/chat/components/self";
import { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { getChats } from '@utils/storage';
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getChats } from "@utils/storage";
import useSWR from 'swr';
import useSWR from "swr";
const fetcher = ([, account]) => getChats(account);
export default function ChatsList() {
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 (
<div className="flex flex-col gap-px">
@ -18,16 +21,18 @@ export default function ChatsList() {
{!chats || error ? (
<>
<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="h-3 w-full animate-pulse 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="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="h-3 w-full animate-pulse 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>
</>
) : (
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>
);

View File

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

View File

@ -1,18 +1,20 @@
import { ImagePicker } from '@shared/form/imagePicker';
import { RelayContext } from '@shared/relayProvider';
import { ImagePicker } from "@shared/form/imagePicker";
import { RelayContext } from "@shared/relayProvider";
import { chatContentAtom } from '@stores/chat';
import { WRITEONLY_RELAYS } from '@stores/constants';
import { chatContentAtom } from "@stores/chat";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date';
import { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtom } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { getEventHash, nip04, signEvent } from 'nostr-tools';
import { useCallback, useContext } from 'react';
import { useAtom } from "jotai";
import { useResetAtom } from "jotai/utils";
import { getEventHash, nip04, signEvent } from "nostr-tools";
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 { account, isLoading, isError } = useActiveAccount();
@ -23,7 +25,7 @@ export default function ChatMessageForm({ receiverPubkey }: { receiverPubkey: st
async (privkey: string) => {
return await nip04.encrypt(privkey, receiverPubkey, value);
},
[receiverPubkey, value]
[receiverPubkey, value],
);
const submitEvent = () => {
@ -35,7 +37,7 @@ export default function ChatMessageForm({ receiverPubkey }: { receiverPubkey: st
created_at: dateToUnix(),
kind: 4,
pubkey: account.pubkey,
tags: [['p', receiverPubkey]],
tags: [["p", receiverPubkey]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
@ -49,7 +51,7 @@ export default function ChatMessageForm({ receiverPubkey }: { receiverPubkey: st
};
const handleEnterPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
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 items-center gap-2 divide-x divide-zinc-700">
<ImagePicker type="chat" />
<div className="flex items-center gap-2 pl-2"></div>
<div className="flex items-center gap-2 pl-2" />
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false}
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"

View File

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

View File

@ -1,26 +1,29 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants';
import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/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);
return (
<div className="group flex items-start gap-3">
{isError || isLoading ? (
<>
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div>
<div className="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 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>
</>
@ -28,7 +31,9 @@ export default function ChatMessageUser({ pubkey, time }: { pubkey: string; time
<>
<div className="relative h-9 w-9 shrink rounded-md">
<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}
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)}
</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>
</>

View File

@ -1,12 +1,12 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { usePageContext } from '@utils/hooks/usePageContext';
import { shortenKey } from '@utils/shortenKey';
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { usePageContext } from "@utils/hooks/usePageContext";
import { shortenKey } from "@utils/shortenKey";
import { twMerge } from 'tailwind-merge';
import { twMerge } from "tailwind-merge";
export default function ChatsListSelfItem() {
const pageContext = usePageContext();
@ -22,17 +22,19 @@ export default function ChatsListSelfItem() {
{isError && <div>error</div>}
{isLoading && !account ? (
<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 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>
) : (
<a
href={`/app/chat?pubkey=${account.pubkey}`}
className={twMerge(
'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' : ''
"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"
: "",
)}
>
<div className="relative h-5 w-5 shrink-0 rounded">
@ -44,7 +46,9 @@ export default function ChatsListSelfItem() {
</div>
<div>
<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>
</h5>
</div>

View File

@ -1,11 +1,15 @@
import { nip04 } from 'nostr-tools';
import { useCallback, useEffect, useState } from 'react';
import { nip04 } from "nostr-tools";
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 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) {
return data.pubkey;
} else {

View File

@ -1,6 +1,6 @@
import AppHeader from '@shared/appHeader';
import MultiAccounts from '@shared/multiAccounts';
import Navigation from '@shared/navigation';
import AppHeader from "@shared/appHeader";
import MultiAccounts from "@shared/multiAccounts";
import Navigation from "@shared/navigation";
export function LayoutChat({ children }: { children: React.ReactNode }) {
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">
<Navigation />
</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>

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { RelayContext } from '@shared/relayProvider';
import { RelayContext } from "@shared/relayProvider";
import LumeIcon from '@icons/lume';
import LumeIcon from "@icons/lume";
import { READONLY_RELAYS } from '@stores/constants';
import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from '@utils/date';
import { dateToUnix, getHourAgo } from "@utils/date";
import {
addToBlacklist,
countTotalLongNotes,
@ -14,11 +14,11 @@ import {
getActiveAccount,
getLastLogin,
updateLastLogin,
} from '@utils/storage';
import { getParentID, nip02ToArray } from '@utils/transform';
} from "@utils/storage";
import { getParentID, nip02ToArray } from "@utils/transform";
import { useContext, useEffect, useRef } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
import { useContext, useEffect, useRef } from "react";
import { navigate } from "vite-plugin-ssr/client/router";
export function Page() {
const pool: any = useContext(RelayContext);
@ -71,7 +71,7 @@ export function Page() {
// kind 4 (chats) query
query.push({
kinds: [4],
'#p': [account.pubkey],
"#p": [account.pubkey],
since: 0,
until: dateToUnix(now.current),
});
@ -98,7 +98,7 @@ export function Page() {
(event: any) => {
switch (event.kind) {
// short text note
case 1:
case 1: {
const parentID = getParentID(event.tags, event.id);
// insert event to local database
createNote(
@ -109,9 +109,10 @@ export function Page() {
event.tags,
event.content,
event.created_at,
parentID
parentID,
);
break;
}
// chat
case 4:
if (event.pubkey !== account.pubkey) {
@ -128,18 +129,18 @@ export function Page() {
event.tags,
event.content,
event.created_at,
event.id
event.id,
);
break;
// hide message (channel only)
case 43:
if (event.tags[0][0] === 'e') {
if (event.tags[0][0] === "e") {
addToBlacklist(account.id, event.tags[0][1], 43, 1);
}
break;
// mute user (channel only)
case 44:
if (event.tags[0][0] === 'p') {
if (event.tags[0][0] === "p") {
addToBlacklist(account.id, event.tags[0][1], 44, 1);
}
break;
@ -152,7 +153,7 @@ export function Page() {
event.tags,
event.content,
event.created_at,
''
"",
);
break;
// long post
@ -166,7 +167,7 @@ export function Page() {
event.tags,
event.content,
event.created_at,
''
"",
);
break;
default:
@ -177,9 +178,9 @@ export function Page() {
() => {
updateLastLogin(dateToUnix(now.current));
timeout = setTimeout(() => {
navigate('/app/today', { overwriteLastHistoryEntry: true });
navigate("/app/today", { overwriteLastHistoryEntry: true });
}, 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="relative h-full overflow-hidden">
{/* 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 */}
<div className="relative flex h-full flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2">
@ -207,7 +211,8 @@ export function Page() {
Here&apos;s an interesting fact:
</h3>
<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>
</div>
</div>
@ -218,12 +223,20 @@ export function Page() {
fill="none"
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
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
/>
</svg>
</div>
</div>

View File

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

View File

@ -1,25 +1,28 @@
import { Kind1 } from '@app/note/components/kind1';
import { Kind1063 } from '@app/note/components/kind1063';
import NoteMetadata from '@app/note/components/metadata';
import { NoteParent } from '@app/note/components/parent';
import { NoteDefaultUser } from '@app/note/components/user/default';
import { NoteWrapper } from '@app/note/components/wrapper';
import { Kind1 } from "@app/note/components/kind1";
import { Kind1063 } from "@app/note/components/kind1063";
import NoteMetadata from "@app/note/components/metadata";
import { NoteParent } from "@app/note/components/parent";
import { NoteDefaultUser } from "@app/note/components/user/default";
import { NoteWrapper } from "@app/note/components/wrapper";
import { noteParser } from '@utils/parser';
import { isTagsIncludeID } from '@utils/transform';
import { noteParser } from "@utils/parser";
import { isTagsIncludeID } from "@utils/transform";
import { useMemo } from 'react';
import { useMemo } from "react";
export function NoteBase({ event }: { event: any }) {
const content = useMemo(() => noteParser(event), [event]);
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 (
<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">
{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} />
) : (
<></>

View File

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

View File

@ -1,4 +1,4 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
function isImage(url: string) {
return /\.(jpg|jpeg|gif|png|webp|avif)$/.test(url);
@ -9,7 +9,13 @@ export function Kind1063({ metadata }: { metadata: string[] }) {
return (
<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>
);
}

View File

@ -1,22 +1,24 @@
import { Kind1 } from '@app/note/components/kind1';
import { Kind1063 } from '@app/note/components/kind1063';
import { NoteSkeleton } from '@app/note/components/skeleton';
import { NoteDefaultUser } from '@app/note/components/user/default';
import { NoteWrapper } from '@app/note/components/wrapper';
import { Kind1 } from "@app/note/components/kind1";
import { Kind1063 } from "@app/note/components/kind1063";
import { NoteSkeleton } from "@app/note/components/skeleton";
import { NoteDefaultUser } from "@app/note/components/user/default";
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 useSWRSubscription from 'swr/subscription';
import { memo, useContext } from "react";
import useSWRSubscription from "swr/subscription";
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
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(
[
{
@ -31,19 +33,23 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
undefined,
{
unsubscribeOnEose: true,
}
},
);
return () => {
unsubscribe();
};
});
},
);
const kind1 = !error && data?.kind === 1 ? noteParser(data) : null;
const kind1063 = !error && data?.kind === 1063 ? data.tags : null;
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 ? (
<>
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />

View File

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

View File

@ -1,28 +1,31 @@
import NoteLike from '@app/note/components/metadata/like';
import NoteReply from '@app/note/components/metadata/reply';
import NoteRepost from '@app/note/components/metadata/repost';
import NoteLike from "@app/note/components/metadata/like";
import NoteReply from "@app/note/components/metadata/reply";
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 useSWRSubscription from 'swr/subscription';
import { useContext, useState } from "react";
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 [replies, setReplies] = useState(0);
const [reposts, setReposts] = 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(
[
{
'#e': [key],
"#e": [key],
since: 0,
kinds: [1, 6, 7],
limit: 20,
@ -38,14 +41,14 @@ export default function NoteMetadata({ id, eventPubkey }: { id: string; eventPub
setReposts((reposts) => reposts + 1);
break;
case 7:
if (event.content === '🤙' || event.content === '+') {
if (event.content === "🤙" || event.content === "+") {
setLikes((likes) => likes + 1);
}
break;
default:
break;
}
}
},
);
return () => {
@ -58,9 +61,18 @@ export default function NoteMetadata({ id, eventPubkey }: { id: string; eventPub
<NoteReply id={id} replies={replies} />
<NoteLike id={id} pubkey={eventPubkey} likes={likes} />
<NoteRepost id={id} pubkey={eventPubkey} reposts={reposts} />
<button 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
type="button"
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>
</div>
);

View File

@ -1,16 +1,20 @@
import { RelayContext } from '@shared/relayProvider';
import { RelayContext } from "@shared/relayProvider";
import LikeIcon from '@icons/like';
import LikeIcon from "@icons/like";
import { WRITEONLY_RELAYS } from '@stores/constants';
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date';
import { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, signEvent } from 'nostr-tools';
import { useContext, useEffect, useState } from 'react';
import { getEventHash, signEvent } from "nostr-tools";
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 { account, isLoading, isError } = useActiveAccount();
@ -21,11 +25,11 @@ export default function NoteLike({ id, pubkey, likes }: { id: string; pubkey: st
if (!isLoading && !isError && account) {
const event: any = {
content: '+',
content: "+",
kind: 7,
tags: [
['e', id],
['p', pubkey],
["e", id],
["p", pubkey],
],
created_at: dateToUnix(),
pubkey: account.pubkey,
@ -37,7 +41,7 @@ export default function NoteLike({ id, pubkey, likes }: { id: string; pubkey: st
// update state
setCount(count + 1);
} else {
console.log('error');
console.log("error");
}
};
@ -46,9 +50,19 @@ export default function NoteLike({ id, pubkey, likes }: { id: string; pubkey: st
}, [likes]);
return (
<button type="button" 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
type="button"
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>
);
}

View File

@ -1,23 +1,26 @@
import { Image } from '@shared/image';
import { RelayContext } from '@shared/relayProvider';
import { Image } from "@shared/image";
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 { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from '@headlessui/react';
import { getEventHash, signEvent } from 'nostr-tools';
import { Fragment, useContext, useEffect, useState } from 'react';
import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, signEvent } from "nostr-tools";
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 [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState('');
const [value, setValue] = useState("");
const { account, isLoading, isError } = useActiveAccount();
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(),
kind: 1,
pubkey: account.pubkey,
tags: [['e', id]],
tags: [["e", id]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
@ -48,7 +51,7 @@ export default function NoteReply({ id, replies }: { id: string; replies: number
setIsOpen(false);
setCount(count + 1);
} else {
console.log('error');
console.log("error");
}
};
@ -58,9 +61,19 @@ export default function NoteReply({ id, replies }: { id: string; replies: number
return (
<>
<button type="button" 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
type="button"
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>
<Transition appear show={isOpen} as={Fragment}>
<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>
<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 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="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 pl-2"></div>
<div className="flex items-center gap-2 pl-2" />
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false}
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-md shadow-fuchsia-900/50 hover:bg-fuchsia-600"

View File

@ -1,16 +1,20 @@
import { RelayContext } from '@shared/relayProvider';
import { RelayContext } from "@shared/relayProvider";
import RepostIcon from '@icons/repost';
import RepostIcon from "@icons/repost";
import { WRITEONLY_RELAYS } from '@stores/constants';
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date';
import { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, signEvent } from 'nostr-tools';
import { useContext, useEffect, useState } from 'react';
import { getEventHash, signEvent } from "nostr-tools";
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 { account, isLoading, isError } = useActiveAccount();
@ -21,11 +25,11 @@ export default function NoteRepost({ id, pubkey, reposts }: { id: string; pubkey
if (!isLoading && !isError && account) {
const event: any = {
content: '',
content: "",
kind: 6,
tags: [
['e', id],
['p', pubkey],
["e", id],
["p", pubkey],
],
created_at: dateToUnix(),
pubkey: account.pubkey,
@ -37,7 +41,7 @@ export default function NoteRepost({ id, pubkey, reposts }: { id: string; pubkey
// update state
setCount(count + 1);
} else {
console.log('error');
console.log("error");
}
};
@ -46,9 +50,19 @@ export default function NoteRepost({ id, pubkey, reposts }: { id: string; pubkey
}, [reposts]);
return (
<button type="button" 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
type="button"
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>
);
}

View File

@ -1,22 +1,24 @@
import { Kind1 } from '@app/note/components/kind1';
import { Kind1063 } from '@app/note/components/kind1063';
import NoteMetadata from '@app/note/components/metadata';
import { NoteSkeleton } from '@app/note/components/skeleton';
import { NoteDefaultUser } from '@app/note/components/user/default';
import { Kind1 } from "@app/note/components/kind1";
import { Kind1063 } from "@app/note/components/kind1063";
import NoteMetadata from "@app/note/components/metadata";
import { NoteSkeleton } from "@app/note/components/skeleton";
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 useSWRSubscription from 'swr/subscription';
import { memo, useContext } from "react";
import useSWRSubscription from "swr/subscription";
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
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(
[
{
@ -31,20 +33,21 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
undefined,
{
unsubscribeOnEose: true,
}
},
);
return () => {
unsubscribe();
};
});
},
);
const kind1 = !error && data?.kind === 1 ? noteParser(data) : null;
const kind1063 = !error && data?.kind === 1063 ? data.tags : null;
return (
<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 ? (
<>
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />

View File

@ -1,10 +1,14 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
export default function ImagePreview({ urls }: { urls: string[] }) {
return (
<div className="mt-3 grid h-full w-full grid-cols-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>
);

View File

@ -1,9 +1,9 @@
import { MediaOutlet, MediaPlayer } from '@vidstack/react';
import { MediaOutlet, MediaPlayer } from "@vidstack/react";
export default function VideoPreview({ urls }: { urls: string[] }) {
return (
<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"
>
<MediaPlayer src={urls[0]} poster="" controls>

View File

@ -1,17 +1,20 @@
import { RootNote } from '@app/note/components/rootNote';
import { NoteRepostUser } from '@app/note/components/user/repost';
import { NoteWrapper } from '@app/note/components/wrapper';
import { RootNote } from "@app/note/components/rootNote";
import { NoteRepostUser } from "@app/note/components/user/repost";
import { NoteWrapper } from "@app/note/components/wrapper";
import { getQuoteID } from '@utils/transform';
import { getQuoteID } from "@utils/transform";
export function NoteQuoteRepost({ event }: { event: any }) {
const rootID = getQuoteID(event.tags);
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="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} />
</div>
<RootNote id={rootID} fallback={event.content} />

View File

@ -1,19 +1,19 @@
import { Image } from '@shared/image';
import { RelayContext } from '@shared/relayProvider';
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import { WRITEONLY_RELAYS } from '@stores/constants';
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from '@utils/date';
import { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, signEvent } from 'nostr-tools';
import { useContext, useState } from 'react';
import { getEventHash, signEvent } from "nostr-tools";
import { useContext, useState } from "react";
export default function NoteReplyForm({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount();
const [value, setValue] = useState('');
const [value, setValue] = useState("");
const profile = account ? JSON.parse(account.metadata) : null;
const submitEvent = () => {
@ -23,7 +23,7 @@ export default function NoteReplyForm({ id }: { id: string }) {
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [['e', id]],
tags: [["e", id]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
@ -31,9 +31,9 @@ export default function NoteReplyForm({ id }: { id: string }) {
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset form
setValue('');
setValue("");
} 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>
<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 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 className="absolute bottom-2 w-full px-2">
<div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700"></div>
<div className="flex items-center gap-2 divide-x divide-zinc-700" />
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false}
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"

View File

@ -1,7 +1,7 @@
import { Kind1 } from '@app/note/components/kind1';
import NoteReplyUser from '@app/note/components/user/reply';
import { Kind1 } from "@app/note/components/kind1";
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 }) {
const content = noteParser(data);

View File

@ -1,24 +1,26 @@
import NoteReplyForm from '@app/note/components/replies/form';
import Reply from '@app/note/components/replies/item';
import NoteReplyForm from "@app/note/components/replies/form";
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 useSWRSubscription from 'swr/subscription';
import { useContext } from "react";
import useSWRSubscription from "swr/subscription";
export default function RepliesList({ id }: { id: string }) {
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
const unsubscribe = pool.subscribe(
[
{
'#e': [key],
"#e": [key],
since: 0,
kinds: [1, 1063],
limit: 20,
@ -27,13 +29,14 @@ export default function RepliesList({ id }: { id: string }) {
READONLY_RELAYS,
(event: any) => {
next(null, (prev: any) => (prev ? [...prev, event] : [event]));
}
},
);
return () => {
unsubscribe();
};
});
},
);
return (
<div className="mt-5">
@ -46,12 +49,12 @@ export default function RepliesList({ id }: { id: string }) {
{error && <div>failed to load</div>}
{!data ? (
<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 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 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>
) : (

View File

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

View File

@ -1,25 +1,30 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants';
import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import { Popover, Transition } from '@headlessui/react';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Fragment } from 'react';
import { Popover, Transition } from "@headlessui/react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { Fragment } from "react";
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);
return (
<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">
<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}
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 flex-col gap-0.5">
<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>
<div className="flex items-baseline gap-1.5 text-sm leading-none text-zinc-500">
<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">
<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"
>
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
<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}
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">
<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>
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)}
</h5>
<span className="truncate text-sm leading-none text-zinc-500">
@ -68,7 +77,9 @@ export function NoteDefaultUser({ pubkey, time }: { pubkey: string; time: number
</span>
</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>

View File

@ -1,23 +1,28 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants';
import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/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);
return (
<div className="group flex items-start gap-2.5">
<div className="relative h-9 w-9 shrink-0 rounded-md">
<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}
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)}
</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>

View File

@ -1,39 +1,48 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants';
import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import { Popover, Transition } from '@headlessui/react';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Fragment } from 'react';
import { Popover, Transition } from "@headlessui/react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { Fragment } from "react";
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);
return (
<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">
<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}
className="h-9 w-9 rounded-md object-cover"
/>
</Popover.Button>
<div className="flex items-baseline gap-1.5 text-sm">
<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">
{' '}
{" "}
reposted
</span>
</h5>
<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>
<Transition
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">
<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"
>
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
<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}
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">
<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>
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)}
</h5>
<span className="truncate text-sm leading-none text-zinc-500">
@ -67,7 +78,9 @@ export function NoteRepostUser({ pubkey, time }: { pubkey: string; time: number
</span>
</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>

View File

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

View File

@ -1,6 +1,6 @@
import AppHeader from '@shared/appHeader';
import MultiAccounts from '@shared/multiAccounts';
import Navigation from '@shared/navigation';
import AppHeader from "@shared/appHeader";
import MultiAccounts from "@shared/multiAccounts";
import Navigation from "@shared/navigation";
export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
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">
<Navigation />
</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>

View File

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

View File

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

View File

@ -1,13 +1,22 @@
import { CreateViewModal } from '@app/today/components/views/createModal';
import { CreateViewModal } from "@app/today/components/views/createModal";
export function Header() {
return (
<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">
<span className="px-2 text-sm font-semibold text-zinc-300">Following</span>
<button
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>
<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
className="inline-block h-6 w-6 rounded ring-2 ring-zinc-950"
src="https://void.cat/d/3Bp6jSHURFNQ9u3pK8nwtq.webp"
@ -37,7 +46,11 @@ export function Header() {
/>
</div>
<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>
<CreateViewModal />
</div>

View File

@ -1,8 +1,8 @@
import CancelIcon from '@icons/cancel';
import PlusIcon from '@icons/plus';
import CancelIcon from "@icons/cancel";
import PlusIcon from "@icons/plus";
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, useState } from 'react';
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
export function CreateViewModal() {
const [isOpen, setIsOpen] = useState(false);
@ -63,19 +63,22 @@ export function CreateViewModal() {
<button
type="button"
onClick={closeModal}
autoFocus={false}
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>
</div>
<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
people in a view.
View is specific feature help you pin who you want to see
in your feed. You can add maximum 5 people in a view.
</Dialog.Description>
</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>
</Transition.Child>
</div>

View File

@ -1,6 +1,6 @@
import AppHeader from '@shared/appHeader';
import MultiAccounts from '@shared/multiAccounts';
import Navigation from '@shared/navigation';
import AppHeader from "@shared/appHeader";
import MultiAccounts from "@shared/multiAccounts";
import Navigation from "@shared/navigation";
export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
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">
<Navigation />
</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>

View File

@ -1,20 +1,28 @@
import { NoteBase } from '@app/note/components/base';
import { NoteQuoteRepost } from '@app/note/components/quoteRepost';
import { NoteSkeleton } from '@app/note/components/skeleton';
import { Header } from '@app/today/components/header';
import { NoteBase } from "@app/note/components/base";
import { NoteQuoteRepost } from "@app/note/components/quoteRepost";
import { NoteSkeleton } from "@app/note/components/skeleton";
import { Header } from "@app/today/components/header";
import { getNotes } from '@utils/storage';
import { getNotes } from "@utils/storage";
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useEffect, useRef } from 'react';
import { useInfiniteQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useEffect, useRef } from "react";
const ITEM_PER_PAGE = 10;
const TIME = Math.floor(Date.now() / 1000);
export function Page() {
const { status, error, data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage }: any = useInfiniteQuery({
queryKey: ['following'],
const {
status,
error,
data,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
}: any = useInfiniteQuery({
queryKey: ["following"],
queryFn: async ({ pageParam = 0 }) => {
return await getNotes(TIME, ITEM_PER_PAGE, pageParam);
},
@ -40,7 +48,11 @@ export function Page() {
return;
}
if (lastItem.index >= allRows.length - 1 && hasNextPage && !isFetchingNextPage) {
if (
lastItem.index >= allRows.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -50,19 +62,19 @@ export function Page() {
<div
ref={parentRef}
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">
<Header />
</div>
<div className="flex-1">
{status === 'loading' ? (
{status === "loading" ? (
<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">
<NoteSkeleton />
</div>
</div>
) : status === 'error' ? (
) : status === "error" ? (
<div>{error.message}</div>
) : (
<div
@ -74,7 +86,10 @@ export function Page() {
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin}px)`,
transform: `translateY(${
itemsVirtualizer[0].start -
rowVirtualizer.options.scrollMargin
}px)`,
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
@ -82,13 +97,21 @@ export function Page() {
if (note) {
if (note.kind === 1) {
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} />
</div>
);
} else {
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} />
</div>
);

View File

@ -1,10 +1,10 @@
import { StrictMode } from 'react';
import { Root, createRoot, hydrateRoot } from 'react-dom/client';
import 'vidstack/styles/defaults.css';
import { StrictMode } from "react";
import { Root, createRoot, hydrateRoot } from "react-dom/client";
import "vidstack/styles/defaults.css";
import './index.css';
import { Shell } from './shell';
import { PageContextClient } from './types';
import "./index.css";
import { Shell } from "./shell";
import { PageContextClient } from "./types";
export const clientRouting = true;
export const hydrationCanBeAborted = true;
@ -14,7 +14,10 @@ let root: Root;
export async function render(pageContext: PageContextClient) {
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 = (
<StrictMode>
@ -24,9 +27,9 @@ export async function render(pageContext: PageContextClient) {
</StrictMode>
);
const container = document.getElementById('app');
const container = document.getElementById("app");
// SPA
if (container.innerHTML === '' || !pageContext.isHydration) {
if (container.innerHTML === "" || !pageContext.isHydration) {
if (!root) {
root = createRoot(container);
}

View File

@ -1,29 +1,32 @@
import { StrictMode } from 'react';
import ReactDOMServer from 'react-dom/server';
import { dangerouslySkipEscape, escapeInject } from 'vite-plugin-ssr/server';
import { StrictMode } from "react";
import ReactDOMServer from "react-dom/server";
import { dangerouslySkipEscape, escapeInject } from "vite-plugin-ssr/server";
import { Shell } from './shell';
import { PageContextServer } from './types';
import { Shell } from "./shell";
import { PageContextServer } from "./types";
export const passToClient = ['pageProps'];
export const passToClient = ["pageProps"];
export function render(pageContext: PageContextServer) {
let pageHtml: string;
if (!pageContext.Page) {
// SPA
pageHtml = '';
pageHtml = "";
} else {
// SSR / HTML-only
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(
<StrictMode>
<Shell pageContext={pageContext}>
<Page {...pageProps} />
</Shell>
</StrictMode>
</StrictMode>,
);
}

View File

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

View File

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

View File

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

View File

@ -1,12 +1,15 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from '@stores/constants';
import { DEFAULT_AVATAR } from "@stores/constants";
export default function ActiveAccount({ user }: { user: any }) {
const userData = JSON.parse(user.metadata);
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
src={userData.picture || DEFAULT_AVATAR}
alt="user's avatar"

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import ArrowLeftIcon from '@icons/arrowLeft';
import ArrowRightIcon from '@icons/arrowRight';
import RefreshIcon from '@icons/refresh';
import ArrowLeftIcon from "@icons/arrowLeft";
import ArrowRightIcon from "@icons/arrowRight";
import RefreshIcon from "@icons/refresh";
export default function AppHeader() {
const goBack = () => {
@ -16,37 +16,57 @@ export default function AppHeader() {
};
return (
<div 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
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">
<button
type="button"
onClick={() => goBack()}
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
type="button"
onClick={() => goForward()}
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>
</div>
<div>
<input
autoCapitalize="none"
autoCorrect="off"
autoFocus={false}
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"
/>
</div>
<div className="flex h-full items-center gap-2">
<button
type="button"
onClick={() => reload()}
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>
</div>
</div>

View File

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

View File

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

View File

@ -1,18 +1,18 @@
import { Post } from '@shared/composer/types/post';
import { User } from '@shared/composer/user';
import { Post } from "@shared/composer/types/post";
import { User } from "@shared/composer/user";
import CancelIcon from '@icons/cancel';
import ChevronDownIcon from '@icons/chevronDown';
import ChevronRightIcon from '@icons/chevronRight';
import ComposeIcon from '@icons/compose';
import CancelIcon from "@icons/cancel";
import ChevronDownIcon from "@icons/chevronDown";
import ChevronRightIcon from "@icons/chevronRight";
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 { useAtom } from 'jotai';
import { Fragment, useState } from 'react';
import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from "jotai";
import { Fragment, useState } from "react";
export function ComposerModal() {
const [isOpen, setIsOpen] = useState(false);
@ -32,7 +32,6 @@ export function ComposerModal() {
<>
<button
type="button"
autoFocus={false}
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"
>
@ -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">
<div className="flex items-center justify-between px-4 py-4">
<div className="flex items-center gap-2">
<div>{!isLoading && !isError && account && <User data={account} />}</div>
<div>
{!isLoading && !isError && account && (
<User data={account} />
)}
</div>
<span>
<ChevronRightIcon width={14} height={14} className="text-zinc-500" />
<ChevronRightIcon
width={14}
height={14}
className="text-zinc-500"
/>
</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">
New Post
@ -75,13 +82,19 @@ export function ComposerModal() {
</div>
</div>
<div
onClick={closeModal}
onKeyDown={closeModal}
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>
{composer.type === 'post' && account && <Post pubkey={account.pubkey} privkey={account.privkey} />}
{composer.type === "post" && account && (
<Post pubkey={account.pubkey} privkey={account.privkey} />
)}
</Dialog.Panel>
</Transition.Child>
</div>

View File

@ -1,28 +1,38 @@
import { ImageUploader } from '@shared/composer/imageUploader';
import TrashIcon from '@shared/icons/trash';
import { RelayContext } from '@shared/relayProvider';
import { ImageUploader } from "@shared/composer/imageUploader";
import TrashIcon from "@shared/icons/trash";
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 { useCallback, useContext, useMemo, useState } from 'react';
import { Node, Transforms, createEditor } from 'slate';
import { withHistory } from 'slate-history';
import { Editable, ReactEditor, Slate, useSlateStatic, withReact } from 'slate-react';
import { getEventHash, signEvent } from "nostr-tools";
import { useCallback, useContext, useMemo, useState } from "react";
import { Node, Transforms, createEditor } from "slate";
import { withHistory } from "slate-history";
import {
Editable,
ReactEditor,
Slate,
useSlateStatic,
withReact,
} from "slate-react";
const withImages = (editor) => {
const { isVoid } = editor;
editor.isVoid = (element) => {
return element.type === 'image' ? true : isVoid(element);
return element.type === "image" ? true : isVoid(element);
};
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 path = ReactEditor.findPath(editor, element);
@ -30,8 +40,13 @@ const ImagePreview = ({ attributes, children, element }: { attributes: any; chil
<figure {...attributes} className="m-0 mt-3">
{children}
<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
type="button"
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"
>
@ -45,19 +60,22 @@ const ImagePreview = ({ attributes, children, element }: { attributes: any; chil
export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const pool: any = useContext(RelayContext);
const editor = useMemo(() => withReact(withImages(withHistory(createEditor()))), []);
const editor = useMemo(
() => withReact(withImages(withHistory(createEditor()))),
[],
);
const [content, setContent] = useState<Node[]>([
{
children: [
{
text: '',
text: "",
},
],
},
]);
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 = () => {
@ -81,7 +99,7 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const renderElement = useCallback((props: any) => {
switch (props.element.type) {
case 'image':
case "image":
if (props.element.url) {
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 w-full gap-2">
<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 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
@ -111,7 +129,6 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
<ImageUploader />
<button
type="button"
autoFocus={false}
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"
>

View File

@ -1,6 +1,6 @@
import { Image } from '@shared/image';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@stores/constants';
import { DEFAULT_AVATAR, IMGPROXY_URL } from "@stores/constants";
export function User({ data }: { data: any }) {
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="h-8 w-8 shrink-0 overflow-hidden rounded bg-zinc-900">
<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}
className="h-8 w-8 object-cover"
loading="auto"
@ -17,7 +19,7 @@ export function User({ data }: { data: any }) {
</div>
<h5 className="text-sm font-semibold leading-none text-zinc-100">
{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>
</div>

View File

@ -1,18 +1,18 @@
import { RelayContext } from '@shared/relayProvider';
import { RelayContext } from "@shared/relayProvider";
import HeartBeatIcon from '@icons/heartbeat';
import HeartBeatIcon from "@icons/heartbeat";
import { READONLY_RELAYS } from '@stores/constants';
import { hasNewerNoteAtom } from '@stores/note';
import { READONLY_RELAYS } from "@stores/constants";
import { hasNewerNoteAtom } from "@stores/note";
import { dateToUnix } from '@utils/date';
import { useActiveAccount } from '@utils/hooks/useActiveAccount';
import { createChat, createNote, updateAccount } from '@utils/storage';
import { getParentID, nip02ToArray } from '@utils/transform';
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { createChat, createNote, updateAccount } from "@utils/storage";
import { getParentID, nip02ToArray } from "@utils/transform";
import { useSetAtom } from 'jotai';
import { useContext, useRef } from 'react';
import useSWRSubscription from 'swr/subscription';
import { useSetAtom } from "jotai";
import { useContext, useRef } from "react";
import useSWRSubscription from "swr/subscription";
export default function EventCollector() {
const pool: any = useContext(RelayContext);
@ -22,7 +22,9 @@ export default function EventCollector() {
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 followsAsArray = nip02ToArray(follows);
const unsubscribe = pool.subscribe(
@ -38,7 +40,7 @@ export default function EventCollector() {
},
{
kinds: [4],
'#p': [key.pubkey],
"#p": [key.pubkey],
since: dateToUnix(now.current),
},
{
@ -51,10 +53,10 @@ export default function EventCollector() {
switch (event.kind) {
// metadata
case 0:
updateAccount('metadata', event.content, event.pubkey);
updateAccount("metadata", event.content, event.pubkey);
break;
// short text note
case 1:
case 1: {
const parentID = getParentID(event.tags, event.id);
createNote(
event.id,
@ -64,15 +66,16 @@ export default function EventCollector() {
event.tags,
event.content,
event.created_at,
parentID
parentID,
);
// notify user reload to get newer note
setHasNewerNote(true);
break;
}
// contacts
case 3:
// update account's folllows with NIP-02 tag list
updateAccount('follows', event.tags, event.pubkey);
updateAccount("follows", event.tags, event.pubkey);
break;
// chat
case 4:
@ -90,24 +93,34 @@ export default function EventCollector() {
event.tags,
event.content,
event.created_at,
event.id
event.id,
);
break;
// long post
case 30023:
// 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;
default:
break;
}
}
},
);
return () => {
unsubscribe();
};
});
},
);
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">

View File

@ -1,31 +1,31 @@
import PlusIcon from '@icons/plus';
import PlusIcon from "@icons/plus";
import { channelContentAtom } from '@stores/channel';
import { chatContentAtom } from '@stores/chat';
import { noteContentAtom } from '@stores/note';
import { channelContentAtom } from "@stores/channel";
import { chatContentAtom } from "@stores/chat";
import { noteContentAtom } from "@stores/note";
import { createBlobFromFile } from '@utils/createBlobFromFile';
import { createBlobFromFile } from "@utils/createBlobFromFile";
import { open } from '@tauri-apps/api/dialog';
import { Body, fetch } from '@tauri-apps/api/http';
import { useSetAtom } from 'jotai';
import { useState } from 'react';
import { open } from "@tauri-apps/api/dialog";
import { Body, fetch } from "@tauri-apps/api/http";
import { useSetAtom } from "jotai";
import { useState } from "react";
export function ImagePicker({ type }: { type: string }) {
let atom;
switch (type) {
case 'note':
case "note":
atom = noteContentAtom;
break;
case 'chat':
case "chat":
atom = chatContentAtom;
break;
case 'channel':
case "channel":
atom = channelContentAtom;
break;
default:
throw new Error('Invalid type');
throw new Error("Invalid type");
}
const [loading, setLoading] = useState(false);
@ -36,8 +36,8 @@ export function ImagePicker({ type }: { type: string }) {
multiple: false,
filters: [
{
name: 'Image',
extensions: ['png', 'jpeg', 'jpg', 'gif'],
name: "Image",
extensions: ["png", "jpeg", "jpg", "gif"],
},
],
});
@ -48,31 +48,35 @@ export function ImagePicker({ type }: { type: string }) {
} else {
setLoading(true);
const filename = selected.split('/').pop();
const filename = selected.split("/").pop();
const file = await createBlobFromFile(selected);
const buf = await file.arrayBuffer();
const res: { data: { file: { id: string } } } = await fetch('https://void.cat/upload?cli=false', {
method: 'POST',
const res: { data: { file: { id: string } } } = await fetch(
"https://void.cat/upload?cli=false",
{
method: "POST",
timeout: 5,
headers: {
accept: '*/*',
'Content-Type': 'application/octet-stream',
'V-Filename': filename,
'V-Description': 'Upload from https://lume.nu',
'V-Strip-Metadata': 'true',
accept: "*/*",
"Content-Type": "application/octet-stream",
"V-Filename": filename,
"V-Description": "Upload from https://lume.nu",
"V-Strip-Metadata": "true",
},
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);
}
};
return (
<button
type="button"
onClick={() => openFileDialog()}
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"
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
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
/>
</svg>
) : (
<PlusIcon width={16} height={16} className="text-zinc-400" />

View File

@ -1,8 +1,15 @@
import { SVGProps } from 'react';
import { SVGProps } from "react";
export default function ArrowLeftIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
export default function ArrowLeftIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<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
d="M10 18.25L3.75 12M3.75 12L10 5.75M3.75 12H20.25"
stroke="currentColor"

View File

@ -1,8 +1,15 @@
import { SVGProps } from 'react';
import { SVGProps } from "react";
export default function ArrowRightIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
export default function ArrowRightIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<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
d="M14 5.75L20.25 12M20.25 12L14 18.25M20.25 12H3.75"
stroke="currentColor"

View File

@ -1,8 +1,17 @@
import { SVGProps } from 'react';
import { SVGProps } from "react";
export default function BellIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
export default function BellIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<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
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"

View File

@ -1,8 +1,15 @@
import { SVGProps } from 'react';
import { SVGProps } from "react";
export default function CancelIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
export default function CancelIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<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
d="M4.75 4.75L19.25 19.25M19.25 4.75L4.75 19.25"
stroke="currentColor"

View File

@ -1,8 +1,17 @@
import { SVGProps } from 'react';
import { SVGProps } from "react";
export default function CheckCircleIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
export default function CheckCircleIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<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
fillRule="evenodd"
clipRule="evenodd"

View File

@ -1,8 +1,17 @@
import { SVGProps } from 'react';
import { SVGProps } from "react";
export default function ChevronDownIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
export default function ChevronDownIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<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
d="M8 10L12 14L16 10"
stroke="currentColor"

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