rome -> eslint + prettier

This commit is contained in:
Ren Amamiya
2023-07-04 13:24:42 +07:00
parent 744fbd5683
commit a30cf66c2e
187 changed files with 10179 additions and 10066 deletions

49
.eslintrc.js Normal file
View File

@@ -0,0 +1,49 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
paths: ['src'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
env: {
browser: true,
amd: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:jsx-a11y/recommended',
'prettier'
],
plugins: [],
rules: {
'react/react-in-jsx-scope': 'off',
'jsx-a11y/accessible-emoji': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
},
};

9
.prettierignore Normal file
View File

@@ -0,0 +1,9 @@
.tmp
.cache/
coverage/
.nyc_output/
**/.yarn/**
**/.pnp.*
/dist*/
node_modules/
src-tauri/

22
.prettierrc Normal file
View File

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

View File

@@ -7,10 +7,13 @@
"build": "vite build", "build": "vite build",
"tauri": "tauri", "tauri": "tauri",
"add-migrate": "cd src-tauri/ && sqlx migrate add", "add-migrate": "cd src-tauri/ && sqlx migrate add",
"prepare": "husky install" "prepare": "husky install",
"lint": "eslint ./src --fix",
"format": "prettier ./src --write"
}, },
"lint-staged": { "lint-staged": {
"**/*.{js,ts,jsx,tsx}": "rome check --apply" "src/*.{ts, tsx}": "eslint --fix",
"src/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
}, },
"dependencies": { "dependencies": {
"@floating-ui/react": "^0.23.1", "@floating-ui/react": "^0.23.1",
@@ -48,20 +51,29 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "^1.4.0", "@tauri-apps/cli": "^1.4.0",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/node": "^18.16.18", "@types/node": "^18.16.18",
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6", "@types/react-dom": "^18.2.6",
"@types/youtube-player": "^5.5.7", "@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"csstype": "^3.1.2", "csstype": "^3.1.2",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^13.2.3", "lint-staged": "^13.2.3",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"rome": "12.1.0",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vite": "^4.3.9", "vite": "^4.3.9",

698
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"$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,33 +1,36 @@
import "./index.css"; import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { AuthCreateScreen } from "@app/auth/create";
import { CreateStep1Screen } from "@app/auth/create/step-1"; import { AuthCreateScreen } from '@app/auth/create';
import { CreateStep2Screen } from "@app/auth/create/step-2"; import { CreateStep1Screen } from '@app/auth/create/step-1';
import { CreateStep3Screen } from "@app/auth/create/step-3"; import { CreateStep2Screen } from '@app/auth/create/step-2';
import { CreateStep4Screen } from "@app/auth/create/step-4"; import { CreateStep3Screen } from '@app/auth/create/step-3';
import { AuthImportScreen } from "@app/auth/import"; import { CreateStep4Screen } from '@app/auth/create/step-4';
import { ImportStep1Screen } from "@app/auth/import/step-1"; import { AuthImportScreen } from '@app/auth/import';
import { ImportStep2Screen } from "@app/auth/import/step-2"; import { ImportStep1Screen } from '@app/auth/import/step-1';
import { OnboardingScreen } from "@app/auth/onboarding"; import { ImportStep2Screen } from '@app/auth/import/step-2';
import { WelcomeScreen } from "@app/auth/welcome"; import { OnboardingScreen } from '@app/auth/onboarding';
import { ChannelScreen } from "@app/channel"; import { WelcomeScreen } from '@app/auth/welcome';
import { ChatScreen } from "@app/chat"; import { ChannelScreen } from '@app/channel';
import { ErrorScreen } from "@app/error"; import { ChatScreen } from '@app/chat';
import { Root } from "@app/root"; import { ErrorScreen } from '@app/error';
import { AccountSettingsScreen } from "@app/settings/account"; import { Root } from '@app/root';
import { GeneralSettingsScreen } from "@app/settings/general"; import { AccountSettingsScreen } from '@app/settings/account';
import { ShortcutsSettingsScreen } from "@app/settings/shortcuts"; import { GeneralSettingsScreen } from '@app/settings/general';
import { SpaceScreen } from "@app/space"; import { ShortcutsSettingsScreen } from '@app/settings/shortcuts';
import { TrendingScreen } from "@app/trending"; import { SpaceScreen } from '@app/space';
import { UserScreen } from "@app/user"; import { TrendingScreen } from '@app/trending';
import { AppLayout } from "@shared/appLayout"; import { UserScreen } from '@app/user';
import { AuthLayout } from "@shared/authLayout";
import { Protected } from "@shared/protected"; import { AppLayout } from '@shared/appLayout';
import { SettingsLayout } from "@shared/settingsLayout"; import { AuthLayout } from '@shared/authLayout';
import { RouterProvider, createBrowserRouter } from "react-router-dom"; import { Protected } from '@shared/protected';
import { SettingsLayout } from '@shared/settingsLayout';
import './index.css';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/", path: '/',
element: ( element: (
<Protected> <Protected>
<Root /> <Root />
@@ -36,57 +39,57 @@ const router = createBrowserRouter([
errorElement: <ErrorScreen />, errorElement: <ErrorScreen />,
}, },
{ {
path: "/auth", path: '/auth',
element: <AuthLayout />, element: <AuthLayout />,
children: [ children: [
{ path: "welcome", element: <WelcomeScreen /> }, { path: 'welcome', element: <WelcomeScreen /> },
{ path: "onboarding", element: <OnboardingScreen /> }, { path: 'onboarding', element: <OnboardingScreen /> },
{ {
path: "import", path: 'import',
element: <AuthImportScreen />, element: <AuthImportScreen />,
children: [ children: [
{ path: "", element: <ImportStep1Screen /> }, { path: '', element: <ImportStep1Screen /> },
{ path: "step-2", element: <ImportStep2Screen /> }, { path: 'step-2', element: <ImportStep2Screen /> },
], ],
}, },
{ {
path: "create", path: 'create',
element: <AuthCreateScreen />, element: <AuthCreateScreen />,
children: [ children: [
{ path: "", element: <CreateStep1Screen /> }, { path: '', element: <CreateStep1Screen /> },
{ path: "step-2", element: <CreateStep2Screen /> }, { path: 'step-2', element: <CreateStep2Screen /> },
{ path: "step-3", element: <CreateStep3Screen /> }, { path: 'step-3', element: <CreateStep3Screen /> },
{ path: "step-4", element: <CreateStep4Screen /> }, { path: 'step-4', element: <CreateStep4Screen /> },
], ],
}, },
], ],
}, },
{ {
path: "/app", path: '/app',
element: ( element: (
<Protected> <Protected>
<AppLayout /> <AppLayout />
</Protected> </Protected>
), ),
children: [ children: [
{ path: "space", element: <SpaceScreen /> }, { path: 'space', element: <SpaceScreen /> },
{ path: "trending", element: <TrendingScreen /> }, { path: 'trending', element: <TrendingScreen /> },
{ path: "user/:pubkey", element: <UserScreen /> }, { path: 'user/:pubkey', element: <UserScreen /> },
{ path: "chat/:pubkey", element: <ChatScreen /> }, { path: 'chat/:pubkey', element: <ChatScreen /> },
{ path: "channel/:id", element: <ChannelScreen /> }, { path: 'channel/:id', element: <ChannelScreen /> },
], ],
}, },
{ {
path: "/settings", path: '/settings',
element: ( element: (
<Protected> <Protected>
<SettingsLayout /> <SettingsLayout />
</Protected> </Protected>
), ),
children: [ children: [
{ path: "general", element: <GeneralSettingsScreen /> }, { path: 'general', element: <GeneralSettingsScreen /> },
{ path: "shortcuts", element: <ShortcutsSettingsScreen /> }, { path: 'shortcuts', element: <ShortcutsSettingsScreen /> },
{ path: "account", element: <AccountSettingsScreen /> }, { path: 'account', element: <AccountSettingsScreen /> },
], ],
}, },
]); ]);

View File

@@ -1,21 +1,20 @@
import { Image } from "@shared/image"; import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
export function User({ import { DEFAULT_AVATAR } from '@stores/constants';
pubkey,
fallback, import { useProfile } from '@utils/hooks/useProfile';
}: { pubkey: string; fallback?: string }) { import { shortenKey } from '@utils/shortenKey';
export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }) {
const { status, user } = useProfile(pubkey, fallback); const { status, user } = useProfile(pubkey, fallback);
if (status === "loading") { if (status === 'loading') {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative h-10 w-10 shrink-0 rounded-md bg-zinc-800 animate-pulse" /> <div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-zinc-800" />
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start"> <div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
<span className="w-1/2 h-4 rounded bg-zinc-800 animate-pulse" /> <span className="h-4 w-1/2 animate-pulse rounded bg-zinc-800" />
<span className="w-1/3 h-3 rounded bg-zinc-800 animate-pulse" /> <span className="h-3 w-1/3 animate-pulse rounded bg-zinc-800" />
</div> </div>
</div> </div>
); );

View File

@@ -1,4 +1,4 @@
import { Outlet } from "react-router-dom"; import { Outlet } from 'react-router-dom';
export function AuthCreateScreen() { export function AuthCreateScreen() {
return ( return (

View File

@@ -1,16 +1,18 @@
import { createAccount, createBlock } from "@libs/storage"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from "@shared/button"; import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from "@shared/icons"; import { useMemo, useState } from 'react';
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from 'react-router-dom';
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
import { useMemo, useState } from "react"; import { createAccount } from '@libs/storage';
import { useNavigate } from "react-router-dom";
import { Button } from '@shared/button';
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
export function CreateStep1Screen() { export function CreateStep1Screen() {
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [type, setType] = useState("password"); const [type, setType] = useState('password');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const privkey = useMemo(() => generatePrivateKey(), []); const privkey = useMemo(() => generatePrivateKey(), []);
@@ -20,19 +22,25 @@ export function CreateStep1Screen() {
// toggle private key // toggle private key
const showPrivateKey = () => { const showPrivateKey = () => {
if (type === "password") { if (type === 'password') {
setType("text"); setType('text');
} else { } else {
setType("password"); setType('password');
} }
}; };
const account = useMutation({ const account = useMutation({
mutationFn: (data: any) => { mutationFn: (data: {
npub: string;
pubkey: string;
privkey: string;
follows: null | string[][];
is_active: number;
}) => {
return createAccount(data.npub, data.pubkey, data.privkey, null, 1); return createAccount(data.npub, data.pubkey, data.privkey, null, 1);
}, },
onSuccess: (data: any) => { onSuccess: (data) => {
queryClient.setQueryData(["currentAccount"], data); queryClient.setQueryData(['currentAccount'], data);
}, },
}); });
@@ -48,7 +56,7 @@ export function CreateStep1Screen() {
}); });
// redirect to next step // redirect to next step
setTimeout(() => navigate("/auth/create/step-2", { replace: true }), 1200); setTimeout(() => navigate('/auth/create/step-2', { replace: true }), 1200);
}; };
return ( return (
@@ -60,32 +68,28 @@ export function CreateStep1Screen() {
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-base font-semibold text-zinc-400"> <span className="text-base font-semibold text-zinc-400">Public Key</span>
Public Key
</label>
<input <input
readOnly readOnly
value={npub} value={npub}
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100" className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-base font-semibold text-zinc-400"> <span className="text-base font-semibold text-zinc-400">Private Key</span>
Private Key
</label>
<div className="relative"> <div className="relative">
<input <input
readOnly readOnly
type={type} type={type}
value={nsec} value={nsec}
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100" className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/> />
<button <button
type="button" type="button"
onClick={() => showPrivateKey()} onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700" className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
> >
{type === "password" ? ( {type === 'password' ? (
<EyeOffIcon <EyeOffIcon
width={20} width={20}
height={20} height={20}
@@ -105,7 +109,7 @@ export function CreateStep1Screen() {
{loading ? ( {loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : ( ) : (
"Continue →" 'Continue →'
)} )}
</Button> </Button>
</div> </div>

View File

@@ -1,19 +1,21 @@
import { AvatarUploader } from "@shared/avatarUploader"; import { useState } from 'react';
import { BannerUploader } from "@shared/bannerUploader"; import { useForm } from 'react-hook-form';
import { LoaderIcon } from "@shared/icons"; import { useNavigate } from 'react-router-dom';
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants"; import { AvatarUploader } from '@shared/avatarUploader';
import { useOnboarding } from "@stores/onboarding"; import { BannerUploader } from '@shared/bannerUploader';
import { useState } from "react"; import { LoaderIcon } from '@shared/icons';
import { useForm } from "react-hook-form"; import { Image } from '@shared/image';
import { useNavigate } from "react-router-dom";
import { DEFAULT_AVATAR } from '@stores/constants';
import { useOnboarding } from '@stores/onboarding';
export function CreateStep2Screen() { export function CreateStep2Screen() {
const navigate = useNavigate(); const navigate = useNavigate();
const createProfile = useOnboarding((state: any) => state.createProfile); const createProfile = useOnboarding((state: any) => state.createProfile);
const [picture, setPicture] = useState(DEFAULT_AVATAR); const [picture, setPicture] = useState(DEFAULT_AVATAR);
const [banner, setBanner] = useState(""); const [banner, setBanner] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { const {
@@ -33,57 +35,52 @@ export function CreateStep2Screen() {
}; };
createProfile(profile); createProfile(profile);
// redirect to next step // redirect to next step
setTimeout( setTimeout(() => navigate('/auth/create/step-3', { replace: true }), 1200);
() => navigate("/auth/create/step-3", { replace: true }),
1200,
);
} catch { } catch {
console.log("error"); console.log('error');
} }
}; };
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100"> <h1 className="text-xl font-semibold text-zinc-100">Create your profile</h1>
Create your profile
</h1>
</div> </div>
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden"> <div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col mb-0"> <form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
<input <input
type={"hidden"} type={'hidden'}
{...register("picture")} {...register('picture')}
value={picture} value={picture}
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-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<input <input
type={"hidden"} type={'hidden'}
{...register("banner")} {...register('banner')}
value={banner} value={banner}
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-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<div className="relative"> <div className="relative">
<div className="relative w-full h-44 bg-zinc-800"> <div className="relative h-44 w-full bg-zinc-800">
<Image <Image
src={banner} src={banner}
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg" fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
alt="user's banner" alt="user's banner"
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full"> <div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
<BannerUploader setBanner={setBanner} /> <BannerUploader setBanner={setBanner} />
</div> </div>
</div> </div>
<div className="px-4 mb-5"> <div className="mb-5 px-4">
<div className="z-10 relative h-14 w-14 -mt-7"> <div className="relative z-10 -mt-7 h-14 w-14">
<Image <Image
src={picture} src={picture}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt="user's avatar" alt="user's avatar"
className="h-14 w-14 object-cover ring-2 ring-zinc-900 rounded-lg" className="h-14 w-14 rounded-lg object-cover ring-2 ring-zinc-900"
/> />
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full"> <div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
<AvatarUploader setPicture={setPicture} /> <AvatarUploader setPicture={setPicture} />
</div> </div>
</div> </div>
@@ -91,51 +88,60 @@ export function CreateStep2Screen() {
</div> </div>
<div className="flex flex-col gap-4 px-4 pb-4"> <div className="flex flex-col gap-4 px-4 pb-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="name"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Name * Name *
</label> </label>
<input <input
type={"text"} type={'text'}
{...register("name", { {...register('name', {
required: true, required: true,
minLength: 4, minLength: 4,
})} })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="about"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Bio Bio
</label> </label>
<textarea <textarea
{...register("about")} {...register('about')}
spellCheck={false} spellCheck={false}
className="resize-none relative h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="website"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Website Website
</label> </label>
<input <input
type={"text"} type={'text'}
{...register("website", { {...register('website', {
required: false, required: false,
})} })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} disabled={!isDirty || !isValid}
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600" className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
> >
{loading ? ( {loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : ( ) : (
"Continue →" 'Continue →'
)} )}
</button> </button>
</div> </div>

View File

@@ -1,12 +1,15 @@
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { Button } from "@shared/button"; import { Body, fetch } from '@tauri-apps/api/http';
import { LoaderIcon } from "@shared/icons"; import { useContext, useState } from 'react';
import { RelayContext } from "@shared/relayProvider"; import { useNavigate } from 'react-router-dom';
import { useOnboarding } from "@stores/onboarding";
import { Body, fetch } from "@tauri-apps/api/http"; import { Button } from '@shared/button';
import { useAccount } from "@utils/hooks/useAccount"; import { LoaderIcon } from '@shared/icons';
import { useContext, useState } from "react"; import { RelayContext } from '@shared/relayProvider';
import { useNavigate } from "react-router-dom";
import { useOnboarding } from '@stores/onboarding';
import { useAccount } from '@utils/hooks/useAccount';
export function CreateStep3Screen() { export function CreateStep3Screen() {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -15,23 +18,23 @@ export function CreateStep3Screen() {
const { account } = useAccount(); const { account } = useAccount();
const [username, setUsername] = useState(""); const [username, setUsername] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const createNIP05 = async () => { const createNIP05 = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await fetch("https://lume.nu/api/user-create", { const response = await fetch('https://lume.nu/api/user-create', {
method: "POST", method: 'POST',
timeout: 30, timeout: 30,
headers: { headers: {
"Content-Type": "application/json; charset=utf-8", 'Content-Type': 'application/json; charset=utf-8',
}, },
body: Body.json({ body: Body.json({
username: username, username: username,
pubkey: account.pubkey, pubkey: account.pubkey,
lightningAddress: "", lightningAddress: '',
}), }),
}); });
@@ -51,23 +54,21 @@ export function CreateStep3Screen() {
event.publish(); event.publish();
// redirect to step 4 // redirect to step 4
navigate("/auth/create/step-4", { replace: true }); navigate('/auth/create/step-4', { replace: true });
} }
} catch (error) { } catch (error) {
setLoading(false); setLoading(false);
console.error("Error:", error); console.error('Error:', error);
} }
}; };
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100"> <h1 className="text-xl font-semibold text-zinc-100">Create your Lume ID</h1>
Create your Lume ID
</h1>
</div> </div>
<div className="w-full flex flex-col justify-center items-center gap-4"> <div className="flex w-full flex-col items-center justify-center gap-4">
<div className="w-full inline-flex items-center justify-center gap-2 rounded-lg bg-zinc-800"> <div className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-zinc-800">
<input <input
type="text" type="text"
value={username} value={username}
@@ -76,11 +77,9 @@ export function CreateStep3Screen() {
autoCorrect="none" autoCorrect="none"
spellCheck="false" spellCheck="false"
placeholder="satoshi" placeholder="satoshi"
className="relative w-full py-3 pl-3.5 !outline-none placeholder:text-zinc-500 bg-transparent text-zinc-100" className="relative w-full bg-transparent py-3 pl-3.5 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
<span className="text-fuchsia-500 font-semibold pr-3.5"> <span className="pr-3.5 font-semibold text-fuchsia-500">@lume.nu</span>
@lume.nu
</span>
</div> </div>
<Button <Button
preset="large" preset="large"
@@ -90,7 +89,7 @@ export function CreateStep3Screen() {
{loading ? ( {loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : ( ) : (
"Continue →" 'Continue →'
)} )}
</Button> </Button>
</div> </div>

View File

@@ -1,110 +1,114 @@
import { User } from "@app/auth/components/user"; import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { updateAccount } from "@libs/storage"; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { useContext, useState } from 'react';
import { CheckCircleIcon, LoaderIcon } from "@shared/icons"; import { useNavigate } from 'react-router-dom';
import { RelayContext } from "@shared/relayProvider";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { User } from '@app/auth/components/user';
import { useAccount } from "@utils/hooks/useAccount";
import { arrayToNIP02 } from "@utils/transform"; import { updateAccount } from '@libs/storage';
import { useContext, useState } from "react";
import { useNavigate } from "react-router-dom"; import { CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { RelayContext } from '@shared/relayProvider';
import { useAccount } from '@utils/hooks/useAccount';
import { arrayToNIP02 } from '@utils/transform';
const INITIAL_LIST = [ const INITIAL_LIST = [
{ {
pubkey: "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", pubkey: '82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2',
}, },
{ {
pubkey: "a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98", pubkey: 'a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98',
}, },
{ {
pubkey: "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
}, },
{ {
pubkey: "c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0", pubkey: 'c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0',
}, },
{ {
pubkey: "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", pubkey: '6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93',
}, },
{ {
pubkey: "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411", pubkey: 'e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411',
}, },
{ {
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
}, },
{ {
pubkey: "c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15", pubkey: 'c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15',
}, },
{ {
pubkey: "e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42", pubkey: 'e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42',
}, },
{ {
pubkey: "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240", pubkey: '84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240',
}, },
{ {
pubkey: "703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898", pubkey: '703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898',
}, },
{ {
pubkey: "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce", pubkey: 'bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce',
}, },
{ {
pubkey: "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0", pubkey: '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0',
}, },
{ {
pubkey: "c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965", pubkey: 'c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965',
}, },
{ {
pubkey: "c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6", pubkey: 'c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6',
}, },
{ {
pubkey: "6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3", pubkey: '6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3',
}, },
{ {
pubkey: "50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63", pubkey: '50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63',
}, },
{ {
pubkey: "3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594", pubkey: '3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594',
}, },
{ {
pubkey: "6e3f51664e19e082df5217fd4492bb96907405a0b27028671dd7f297b688608c", pubkey: '6e3f51664e19e082df5217fd4492bb96907405a0b27028671dd7f297b688608c',
}, },
{ {
pubkey: "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884", pubkey: '2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884',
}, },
{ {
pubkey: "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", pubkey: '3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24',
}, },
{ {
pubkey: "eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f", pubkey: 'eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f',
}, },
{ {
pubkey: "be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479", pubkey: 'be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479',
}, },
{ {
pubkey: "a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f", pubkey: 'a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f',
}, },
{ {
pubkey: "1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b", pubkey: '1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b',
}, },
{ {
pubkey: "c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5", pubkey: 'c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5',
}, },
{ {
pubkey: "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", pubkey: '460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c',
}, },
{ {
pubkey: "7f3b464b9ff3623630485060cbda3a7790131c5339a7803bde8feb79a5e1b06a", pubkey: '7f3b464b9ff3623630485060cbda3a7790131c5339a7803bde8feb79a5e1b06a',
}, },
{ {
pubkey: "b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27", pubkey: 'b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27',
}, },
{ {
pubkey: "e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2", pubkey: 'e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2',
}, },
{ {
pubkey: "ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14", pubkey: 'ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14',
}, },
{ {
pubkey: "ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609", pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609',
}, },
]; ];
@@ -117,10 +121,10 @@ export function CreateStep4Screen() {
const [follows, setFollows] = useState([]); const [follows, setFollows] = useState([]);
const { account } = useAccount(); const { account } = useAccount();
const { status, data } = useQuery(["trending-profiles"], async () => { const { status, data } = useQuery(['trending-profiles'], async () => {
const res = await fetch("https://api.nostr.band/v0/trending/profiles"); const res = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) { if (!res.ok) {
throw new Error("Error"); throw new Error('Error');
} }
return res.json(); return res.json();
}); });
@@ -135,10 +139,10 @@ export function CreateStep4Screen() {
const update = useMutation({ const update = useMutation({
mutationFn: (follows: any) => { mutationFn: (follows: any) => {
return updateAccount("follows", follows, account.pubkey); return updateAccount('follows', follows, account.pubkey);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["currentAccount"] }); queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
}, },
}); });
@@ -153,7 +157,7 @@ export function CreateStep4Screen() {
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
// build event // build event
event.content = ""; event.content = '';
event.kind = 3; event.kind = 3;
event.pubkey = account.pubkey; event.pubkey = account.pubkey;
event.tags = tags; event.tags = tags;
@@ -164,9 +168,9 @@ export function CreateStep4Screen() {
update.mutate([...follows, account.pubkey]); update.mutate([...follows, account.pubkey]);
// redirect to next step // redirect to next step
setTimeout(() => navigate("/auth/onboarding", { replace: true }), 1200); setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
} catch { } catch {
console.log("error"); console.log('error');
} }
}; };
@@ -180,40 +184,35 @@ export function CreateStep4Screen() {
</h1> </h1>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden"> <div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400"> <div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400">
Follow at least Follow at least
<span className="text-fuchsia-500 font-semibold"> <span className="font-semibold text-fuchsia-500">
{follows.length}/10 {follows.length}/10
</span>{" "} </span>{' '}
plebs plebs
</div> </div>
{status === "loading" ? ( {status === 'loading' ? (
<div className="py-2 px-4 w-full h-11 inline-flex items-center justify-center"> <div className="inline-flex h-11 w-full items-center justify-center px-4 py-2">
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
</div> </div>
) : ( ) : (
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2"> <div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
{list.map( {list.map((item: { pubkey: string; profile: { content: string } }) => (
(item: { pubkey: string; profile: { content: string } }) => (
<button <button
key={item.pubkey} key={item.pubkey}
type="button" type="button"
onClick={() => toggleFollow(item.pubkey)} onClick={() => toggleFollow(item.pubkey)}
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1" className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
> >
<User <User pubkey={item.pubkey} fallback={item.profile?.content} />
pubkey={item.pubkey}
fallback={item.profile?.content}
/>
{follows.includes(item.pubkey) && ( {follows.includes(item.pubkey) && (
<div> <div>
<CheckCircleIcon className="w-4 h-4 text-green-400" /> <CheckCircleIcon className="h-4 w-4 text-green-400" />
</div> </div>
)} )}
</button> </button>
), ))}
)}
</div> </div>
)} )}
</div> </div>
@@ -221,12 +220,12 @@ export function CreateStep4Screen() {
<button <button
type="button" type="button"
onClick={() => submit()} onClick={() => submit()}
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600" className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
> >
{loading ? ( {loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : ( ) : (
"Finish →" 'Finish →'
)} )}
</button> </button>
)} )}

View File

@@ -1,4 +1,4 @@
import { Outlet } from "react-router-dom"; import { Outlet } from 'react-router-dom';
export function AuthImportScreen() { export function AuthImportScreen() {
return ( return (

View File

@@ -1,10 +1,12 @@
import { createAccount, createBlock } from "@libs/storage"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { LoaderIcon } from "@shared/icons"; import { getPublicKey, nip19 } from 'nostr-tools';
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useState } from 'react';
import { getPublicKey, nip19 } from "nostr-tools"; import { Resolver, useForm } from 'react-hook-form';
import { useState } from "react"; import { useNavigate } from 'react-router-dom';
import { Resolver, useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { createAccount } from '@libs/storage';
import { LoaderIcon } from '@shared/icons';
type FormValues = { type FormValues = {
key: string; key: string;
@@ -16,8 +18,8 @@ const resolver: Resolver<FormValues> = async (values) => {
errors: !values.key errors: !values.key
? { ? {
key: { key: {
type: "required", type: 'required',
message: "This is required.", message: 'This is required.',
}, },
} }
: {}, : {},
@@ -35,7 +37,7 @@ export function ImportStep1Screen() {
return createAccount(data.npub, data.pubkey, data.privkey, null, 1); return createAccount(data.npub, data.pubkey, data.privkey, null, 1);
}, },
onSuccess: (data: any) => { onSuccess: (data: any) => {
queryClient.setQueryData(["currentAccount"], data); queryClient.setQueryData(['currentAccount'], data);
}, },
}); });
@@ -50,12 +52,12 @@ export function ImportStep1Screen() {
try { try {
setLoading(true); setLoading(true);
let privkey = data["key"]; let privkey = data['key'];
if (privkey.substring(0, 4) === "nsec") { if (privkey.substring(0, 4) === 'nsec') {
privkey = nip19.decode(privkey).data; privkey = nip19.decode(privkey).data;
} }
if (typeof getPublicKey(privkey) === "string") { if (typeof getPublicKey(privkey) === 'string') {
const pubkey = getPublicKey(privkey); const pubkey = getPublicKey(privkey);
const npub = nip19.npubEncode(pubkey); const npub = nip19.npubEncode(pubkey);
@@ -69,15 +71,12 @@ export function ImportStep1Screen() {
}); });
// redirect to step 2 // redirect to step 2
setTimeout( setTimeout(() => navigate('/auth/import/step-2', { replace: true }), 1200);
() => navigate("/auth/import/step-2", { replace: true }),
1200,
);
} }
} catch (error) { } catch (error) {
setError("key", { setError('key', {
type: "custom", type: 'custom',
message: "Private Key is invalid, please check again", message: 'Private Key is invalid, please check again',
}); });
} }
}; };
@@ -91,10 +90,10 @@ export function ImportStep1Screen() {
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3"> <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<input <input
{...register("key", { required: true, minLength: 32 })} {...register('key', { required: true, minLength: 32 })}
type={"password"} type={'password'}
placeholder="Paste private key here..." placeholder="Paste private key here..."
className="relative w-full rounded-lg px-3 py-3 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative w-full rounded-lg bg-zinc-800 px-3 py-3 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
<span className="text-base text-red-400"> <span className="text-base text-red-400">
{errors.key && <p>{errors.key.message}</p>} {errors.key && <p>{errors.key.message}</p>}
@@ -104,12 +103,12 @@ export function ImportStep1Screen() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} disabled={!isDirty || !isValid}
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600" className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
> >
{loading ? ( {loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : ( ) : (
"Continue →" 'Continue →'
)} )}
</button> </button>
</div> </div>

View File

@@ -1,13 +1,17 @@
import { User } from "@app/auth/components/user"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateAccount } from "@libs/storage"; import { useContext, useState } from 'react';
import { Button } from "@shared/button"; import { useNavigate } from 'react-router-dom';
import { LoaderIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider"; import { User } from '@app/auth/components/user';
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useAccount } from "@utils/hooks/useAccount"; import { updateAccount } from '@libs/storage';
import { setToArray } from "@utils/transform";
import { useContext, useState } from "react"; import { Button } from '@shared/button';
import { useNavigate } from "react-router-dom"; import { LoaderIcon } from '@shared/icons';
import { RelayContext } from '@shared/relayProvider';
import { useAccount } from '@utils/hooks/useAccount';
import { setToArray } from '@utils/transform';
export function ImportStep2Screen() { export function ImportStep2Screen() {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -19,10 +23,10 @@ export function ImportStep2Screen() {
const update = useMutation({ const update = useMutation({
mutationFn: (follows: any) => { mutationFn: (follows: any) => {
return updateAccount("follows", follows, account.pubkey); return updateAccount('follows', follows, account.pubkey);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["currentAccount"] }); queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
}, },
}); });
@@ -41,9 +45,9 @@ export function ImportStep2Screen() {
update.mutate([...followsList, account.pubkey]); update.mutate([...followsList, account.pubkey]);
// redirect to next step // redirect to next step
setTimeout(() => navigate("/auth/onboarding", { replace: true }), 1200); setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
} catch { } catch {
console.log("error"); console.log('error');
} }
}; };
@@ -51,17 +55,17 @@ export function ImportStep2Screen() {
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-xl font-semibold"> <h1 className="text-xl font-semibold">
{loading ? "Creating..." : "Continue with"} {loading ? 'Creating...' : 'Continue with'}
</h1> </h1>
</div> </div>
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4"> <div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4">
{status === "loading" ? ( {status === 'loading' ? (
<div className="w-full"> <div className="w-full">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" /> <div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
<div> <div>
<h3 className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" /> <div 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 className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
</div> </div>
</div> </div>
</div> </div>
@@ -72,7 +76,7 @@ export function ImportStep2Screen() {
{loading ? ( {loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : ( ) : (
"Continue →" 'Continue →'
)} )}
</Button> </Button>
</div> </div>

View File

@@ -1,10 +1,13 @@
import { usePublish } from "@libs/ndk"; import { useState } from 'react';
import { LoaderIcon } from "@shared/icons"; import { Link, useNavigate } from 'react-router-dom';
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
import { User } from "@shared/user"; import { usePublish } from '@libs/ndk';
import { useAccount } from "@utils/hooks/useAccount";
import { useState } from "react"; import { LoaderIcon } from '@shared/icons';
import { Link, useNavigate } from "react-router-dom"; import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { User } from '@shared/user';
import { useAccount } from '@utils/hooks/useAccount';
export function OnboardingScreen() { export function OnboardingScreen() {
const publish = usePublish(); const publish = usePublish();
@@ -20,13 +23,13 @@ export function OnboardingScreen() {
// publish event // publish event
publish({ publish({
content: content:
"Running Lume, fighting for better future, join us here: https://lume.nu", 'Running Lume, fighting for better future, join us here: https://lume.nu',
kind: 1, kind: 1,
tags: [], tags: [],
}); });
// redirect to home // redirect to home
setTimeout(() => navigate("/", { replace: true }), 1200); setTimeout(() => navigate('/', { replace: true }), 1200);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@@ -40,27 +43,24 @@ export function OnboardingScreen() {
👋 Hello, welcome you to Lume 👋 Hello, welcome you to Lume
</h1> </h1>
<p className="text-sm text-zinc-300"> <p className="text-sm text-zinc-300">
You're a part of better future that we're fighting You&apos;re a part of better future that we&apos;re fighting
</p> </p>
<p className="text-sm text-zinc-300"> <p className="text-sm text-zinc-300">
If Lume gets your attention, please help us spread via button below If Lume gets your attention, please help us spread via button below
</p> </p>
</div> </div>
<div className="w-full border-t border-zinc-800/50 bg-zinc-900 rounded-xl"> <div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full px-5 py-3"> <div className="h-min w-full px-5 py-3">
{status === "success" && ( {status === 'success' && (
<User <User pubkey={account.pubkey} time={Math.floor(Date.now() / 1000)} />
pubkey={account.pubkey}
time={Math.floor(Date.now() / 1000)}
/>
)} )}
<div className="-mt-6 pl-[49px] select-text whitespace-pre-line break-words text-base text-zinc-100"> <div className="-mt-6 select-text whitespace-pre-line break-words pl-[49px] text-base text-zinc-100">
<p>Running Lume, fighting for better future</p> <p>Running Lume, fighting for better future</p>
<p> <p>
join us here:{" "} join us here:{' '}
<a <a
href="https://lume.nu" href="https://lume.nu"
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal" className="font-normal text-fuchsia-500 no-underline hover:text-fuchsia-600"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
@@ -70,11 +70,11 @@ export function OnboardingScreen() {
</div> </div>
</div> </div>
</div> </div>
<div className="mt-4 w-full flex flex-col gap-2"> <div className="mt-4 flex w-full flex-col gap-2">
<button <button
type="button" type="button"
onClick={() => submit()} onClick={() => submit()}
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600" className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-zinc-100 hover:bg-fuchsia-600"
> >
{loading ? ( {loading ? (
<> <>
@@ -86,7 +86,7 @@ export function OnboardingScreen() {
<> <>
<span className="w-5" /> <span className="w-5" />
<span>Publish</span> <span>Publish</span>
<ArrowRightCircleIcon className="w-5 h-5" /> <ArrowRightCircleIcon className="h-5 w-5" />
</> </>
)} )}
</button> </button>

View File

@@ -1,49 +1,50 @@
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle"; import { Link } from 'react-router-dom';
import { Link } from "react-router-dom";
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
export function WelcomeScreen() { export function WelcomeScreen() {
return ( return (
<div className="w-full h-full grid grid-cols-12 gap-4 px-4 py-4"> <div className="grid h-full w-full grid-cols-12 gap-4 px-4 py-4">
<div className="col-span-5 border-t border-zinc-800/50 bg-zinc-900 rounded-xl flex flex-col"> <div className="col-span-5 flex flex-col rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="w-full h-full flex flex-col justify-center px-4 py-4 gap-2"> <div className="flex h-full w-full flex-col justify-center gap-2 px-4 py-4">
<h1 className="text-zinc-700 text-4xl font-bold leading-none text-transparent"> <h1 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Preserve your <span className="text-fuchsia-300">freedom</span> Preserve your <span className="text-fuchsia-300">freedom</span>
</h1> </h1>
<h2 className="text-zinc-700 text-4xl font-bold leading-none text-transparent"> <h2 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Protect your <span className="text-red-300">future</span> Protect your <span className="text-red-300">future</span>
</h2> </h2>
<h3 className="text-zinc-700 text-4xl font-bold leading-none text-transparent"> <h3 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Stack <span className="text-orange-300">bitcoin</span> Stack <span className="text-orange-300">bitcoin</span>
</h3> </h3>
<h3 className="text-zinc-700 text-4xl font-bold leading-none text-transparent"> <h3 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Use <span className="text-purple-300">nostr</span> Use <span className="text-purple-300">nostr</span>
</h3> </h3>
</div> </div>
<div className="mt-auto w-full flex flex-col gap-2 px-4 py-4"> <div className="mt-auto flex w-full flex-col gap-2 px-4 py-4">
<Link <Link
to="/auth/import" to="/auth/import"
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600" className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-zinc-100 hover:bg-fuchsia-600"
> >
<span className="w-5" /> <span className="w-5" />
<span>Login with private key</span> <span>Login with private key</span>
<ArrowRightCircleIcon className="w-5 h-5" /> <ArrowRightCircleIcon className="h-5 w-5" />
</Link> </Link>
<Link <Link
to="/auth/create" to="/auth/create"
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg px-6 font-medium text-zinc-200 bg-zinc-800 hover:bg-zinc-700" className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-zinc-800 px-6 font-medium text-zinc-200 hover:bg-zinc-700"
> >
Create new key Create new key
</Link> </Link>
</div> </div>
</div> </div>
<div <div
className="col-span-5 bg-zinc-900 rounded-xl bg-cover bg-center" className="col-span-5 rounded-xl bg-zinc-900 bg-cover bg-center"
style={{ style={{
backgroundImage: `url("https://void.cat/d/Ps1b36vu5pdkEA2w75usuB")`, backgroundImage: `url("https://void.cat/d/Ps1b36vu5pdkEA2w75usuB")`,
}} }}
/> />
<div <div
className="col-span-2 bg-zinc-900 rounded-xl bg-cover bg-center" className="col-span-2 rounded-xl bg-zinc-900 bg-cover bg-center"
style={{ style={{
backgroundImage: `url("https://void.cat/d/5FdJcBP5ZXKAjYqV8hpcp3")`, backgroundImage: `url("https://void.cat/d/5FdJcBP5ZXKAjYqV8hpcp3")`,
}} }}

View File

@@ -1,7 +1,9 @@
import { MutedItem } from "@app/channel/components/mutedItem"; import { Popover, Transition } from '@headlessui/react';
import { Popover, Transition } from "@headlessui/react"; import { Fragment } from 'react';
import { MuteIcon } from "@shared/icons";
import { Fragment } from "react"; import { MutedItem } from '@app/channel/components/mutedItem';
import { MuteIcon } from '@shared/icons';
export function ChannelBlackList({ blacklist }: { blacklist: any }) { export function ChannelBlackList({ blacklist }: { blacklist: any }) {
return ( return (
@@ -10,9 +12,7 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
<> <>
<Popover.Button <Popover.Button
className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 focus:outline-none ${ className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 focus:outline-none ${
open open ? 'bg-zinc-800 hover:bg-zinc-700' : 'bg-zinc-900 hover:bg-zinc-800'
? "bg-zinc-800 hover:bg-zinc-700"
: "bg-zinc-900 hover:bg-zinc-800"
}`} }`}
> >
<MuteIcon <MuteIcon
@@ -31,15 +31,15 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute right-0 z-10 mt-1 w-screen max-w-xs transform px-4 sm:px-0"> <Popover.Panel className="absolute right-0 z-10 mt-1 w-screen max-w-xs transform px-4 sm:px-0">
<div className="flex flex-col gap-2 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900 shadow-popover"> <div className="shadow-popover flex flex-col gap-2 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900">
<div className="h-min w-full shrink-0 border-b border-zinc-800 p-3"> <div className="h-min w-full shrink-0 border-b border-zinc-800 p-3">
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<h3 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text font-semibold leading-none text-transparent"> <h3 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text font-semibold leading-none text-transparent">
Your muted list Your muted list
</h3> </h3>
<p className="text-base leading-tight text-zinc-400"> <p className="text-base leading-tight text-zinc-400">
Currently, unmute only affect locally, when you move to Currently, unmute only affect locally, when you move to new client,
new client, muted list will loaded again muted list will loaded again
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,17 +1,21 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from '@headlessui/react';
import { createChannel } from "@libs/storage"; import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AvatarUploader } from "@shared/avatarUploader"; import { Fragment, useContext, useEffect, useState } from 'react';
import { CancelIcon, LoaderIcon, PlusIcon } from "@shared/icons"; import { useForm } from 'react-hook-form';
import { Image } from "@shared/image"; import { useNavigate } from 'react-router-dom';
import { RelayContext } from "@shared/relayProvider";
import { DEFAULT_AVATAR } from "@stores/constants"; import { createChannel } from '@libs/storage';
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { dateToUnix } from "@utils/date"; import { AvatarUploader } from '@shared/avatarUploader';
import { useAccount } from "@utils/hooks/useAccount"; import { CancelIcon, LoaderIcon, PlusIcon } from '@shared/icons';
import { Fragment, useContext, useEffect, useState } from "react"; import { Image } from '@shared/image';
import { useForm } from "react-hook-form"; import { RelayContext } from '@shared/relayProvider';
import { useNavigate } from "react-router-dom";
import { DEFAULT_AVATAR } from '@stores/constants';
import { dateToUnix } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
export function ChannelCreateModal() { export function ChannelCreateModal() {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -48,11 +52,11 @@ export function ChannelCreateModal() {
event.name, event.name,
event.picture, event.picture,
event.about, event.about,
event.created_at, event.created_at
); );
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["channels"] }); queryClient.invalidateQueries({ queryKey: ['channels'] });
}, },
}); });
@@ -92,12 +96,12 @@ export function ChannelCreateModal() {
navigate(`/app/channel/${event.id}`); navigate(`/app/channel/${event.id}`);
}, 1000); }, 1000);
} catch (e) { } catch (e) {
console.log("error: ", e); console.log('error: ', e);
} }
}; };
useEffect(() => { useEffect(() => {
setValue("picture", image); setValue('picture', image);
}, [setValue, image]); }, [setValue, image]);
return ( return (
@@ -152,34 +156,30 @@ export function ChannelCreateModal() {
onClick={closeModal} onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon <CancelIcon width={20} height={20} className="text-zinc-300" />
width={20}
height={20}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="text-sm leading-tight text-zinc-400"> <Dialog.Description className="text-sm leading-tight text-zinc-400">
Channels are freedom square, everyone can speech freely, Channels are freedom square, everyone can speech freely, no one can
no one can stop you or deceive what to speech stop you or deceive what to speech
</Dialog.Description> </Dialog.Description>
</div> </div>
</div> </div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3"> <div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0" className="mb-0 flex h-full w-full flex-col gap-4"
> >
<input <input
type={"hidden"} type={'hidden'}
{...register("picture")} {...register('picture')}
value={image} value={image}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400"> <span className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Picture Picture
</label> </span>
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950"> <div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
<Image <Image
src={image} src={image}
@@ -188,32 +188,38 @@ export function ChannelCreateModal() {
className="relative z-10 h-11 w-11 rounded-md" className="relative z-10 h-11 w-11 rounded-md"
/> />
<div className="absolute bottom-3 right-3 z-10"> <div className="absolute bottom-3 right-3 z-10">
<AvatarUploader valueState={setImage} /> <AvatarUploader setPicture={setImage} />
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="name"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Channel name * Channel name *
</label> </label>
<input <input
type={"text"} type={'text'}
{...register("name", { {...register('name', {
required: true, required: true,
minLength: 4, minLength: 4,
})} })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="about"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Description Description
</label> </label>
<textarea <textarea
{...register("about")} {...register('about')}
spellCheck={false} spellCheck={false}
className="relative resize-none h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<div className="flex h-20 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2"> <div className="flex h-20 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
@@ -222,8 +228,8 @@ export function ChannelCreateModal() {
Encrypted Encrypted
</span> </span>
<p className="w-4/5 text-sm leading-none text-zinc-400"> <p className="w-4/5 text-sm leading-none text-zinc-400">
All messages are encrypted and only invited members All messages are encrypted and only invited members can view and
can view and send message send message
</p> </p>
</div> </div>
<div> <div>
@@ -242,12 +248,12 @@ export function ChannelCreateModal() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} disabled={!isDirty || !isValid}
className="inline-flex items-center justify-center gap-1 transform active:translate-y-1 disabled:pointer-events-none disabled:opacity-50 focus:outline-none h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600" className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
> >
{loading ? ( {loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : ( ) : (
"Create channel →" 'Create channel →'
)} )}
</button> </button>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile"; import { NavLink } from 'react-router-dom';
import { NavLink } from "react-router-dom"; import { twMerge } from 'tailwind-merge';
import { twMerge } from "tailwind-merge";
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile';
export function ChannelsListItem({ data }: { data: any }) { export function ChannelsListItem({ data }: { data: any }) {
const channel = useChannelProfile(data.event_id); const channel = useChannelProfile(data.event_id);
@@ -10,19 +11,19 @@ export function ChannelsListItem({ data }: { data: any }) {
preventScrollReset={true} preventScrollReset={true}
className={({ isActive }) => className={({ isActive }) =>
twMerge( twMerge(
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5", 'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
isActive ? "bg-zinc-900/50 text-zinc-100" : "", isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
) )
} }
> >
<div className="inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<span className="text-xs text-zinc-100">#</span> <span className="text-xs text-zinc-100">#</span>
</div> </div>
<div className="w-full inline-flex items-center justify-between"> <div className="inline-flex w-full items-center justify-between">
<h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5> <h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5>
<div className="flex items-center"> <div className="flex items-center">
{data.new_messages && ( {data.new_messages && (
<span className="inline-flex items-center justify-center rounded bg-fuchsia-400/10 w-8 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20"> <span className="inline-flex w-8 items-center justify-center rounded bg-fuchsia-400/10 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">
{data.new_messages} {data.new_messages}
</span> </span>
)} )}

View File

@@ -1,7 +1,9 @@
import { ChannelCreateModal } from "@app/channel/components/createModal"; import { useQuery } from '@tanstack/react-query';
import { ChannelsListItem } from "@app/channel/components/item";
import { getChannels } from "@libs/storage"; import { ChannelCreateModal } from '@app/channel/components/createModal';
import { useQuery } from "@tanstack/react-query"; import { ChannelsListItem } from '@app/channel/components/item';
import { getChannels } from '@libs/storage';
export function ChannelsList() { export function ChannelsList() {
const { const {
@@ -9,7 +11,7 @@ export function ChannelsList() {
data: channels, data: channels,
isFetching, isFetching,
} = useQuery( } = useQuery(
["channels"], ['channels'],
async () => { async () => {
return await getChannels(); return await getChannels();
}, },
@@ -17,12 +19,12 @@ export function ChannelsList() {
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}, }
); );
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
{status === "loading" ? ( {status === 'loading' ? (
<> <>
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />

View File

@@ -1,6 +1,8 @@
import { Image } from "@shared/image"; import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
export function Member({ pubkey }: { pubkey: string }) { export function Member({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey); const { user, isError, isLoading } = useProfile(pubkey);

View File

@@ -1,22 +1,21 @@
import { Member } from "@app/channel/components/member"; import { useQuery } from '@tanstack/react-query';
import { getChannelUsers } from "@libs/storage";
import { useQuery } from "@tanstack/react-query"; import { Member } from '@app/channel/components/member';
import { getChannelUsers } from '@libs/storage';
export function ChannelMembers({ id }: { id: string }) { export function ChannelMembers({ id }: { id: string }) {
const { status, data, isFetching } = useQuery( const { status, data, isFetching } = useQuery(['channel-members', id], async () => {
["channel-members", id],
async () => {
return await getChannelUsers(id); return await getChannelUsers(id);
}, });
);
return ( return (
<div className="mt-3"> <div className="mt-3">
<h5 className="border-b border-zinc-900 pb-1 font-semibold text-zinc-200"> <h5 className="border-b border-zinc-900 pb-1 font-semibold text-zinc-200">
Members Members
</h5> </h5>
<div className="mt-3 w-full flex flex-wrap gap-1.5"> <div className="mt-3 flex w-full flex-wrap gap-1.5">
{status === "loading" || isFetching ? ( {status === 'loading' || isFetching ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
data.map((member: { pubkey: string }) => ( data.map((member: { pubkey: string }) => (

View File

@@ -1,17 +1,21 @@
import { UserReply } from "@app/channel/components/messages/userReply"; import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { useContext, useState } from 'react';
import { CancelIcon, EnterIcon } from "@shared/icons";
import { MediaUploader } from "@shared/mediaUploader"; import { UserReply } from '@app/channel/components/messages/userReply';
import { RelayContext } from "@shared/relayProvider";
import { useChannelMessages } from "@stores/channels"; import { CancelIcon, EnterIcon } from '@shared/icons';
import { dateToUnix } from "@utils/date"; import { MediaUploader } from '@shared/mediaUploader';
import { useAccount } from "@utils/hooks/useAccount"; import { RelayContext } from '@shared/relayProvider';
import { useContext, useState } from "react";
import { useChannelMessages } from '@stores/channels';
import { dateToUnix } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
export function ChannelMessageForm({ channelID }: { channelID: string }) { export function ChannelMessageForm({ channelID }: { channelID: string }) {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
const [value, setValue] = useState(""); const [value, setValue] = useState('');
const [replyTo, closeReply] = useChannelMessages((state: any) => [ const [replyTo, closeReply] = useChannelMessages((state: any) => [
state.replyTo, state.replyTo,
state.closeReply, state.closeReply,
@@ -24,12 +28,12 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
if (replyTo.id !== null) { if (replyTo.id !== null) {
tags = [ tags = [
["e", channelID, "", "root"], ['e', channelID, '', 'root'],
["e", replyTo.id, "", "reply"], ['e', replyTo.id, '', 'reply'],
["p", replyTo.pubkey, ""], ['p', replyTo.pubkey, ''],
]; ];
} else { } else {
tags = [["e", channelID, "", "root"]]; tags = [['e', channelID, '', 'root']];
} }
const signer = new NDKPrivateKeySigner(account.privkey); const signer = new NDKPrivateKeySigner(account.privkey);
@@ -47,11 +51,11 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
event.publish(); event.publish();
// reset state // reset state
setValue(""); setValue('');
}; };
const handleEnterPress = (e) => { const handleEnterPress = (e) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
submit(); submit();
} }
@@ -62,7 +66,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
}; };
return ( return (
<div className={`relative w-full ${replyTo.id ? "h-36" : "h-24"}`}> <div className={`relative w-full ${replyTo.id ? 'h-36' : 'h-24'}`}>
{replyTo.id && ( {replyTo.id && (
<div className="absolute left-0 top-0 z-10 h-16 w-full p-[2px]"> <div className="absolute left-0 top-0 z-10 h-16 w-full p-[2px]">
<div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3"> <div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3">
@@ -89,11 +93,11 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
spellCheck={false} spellCheck={false}
placeholder="Message" placeholder="Message"
className={`relative ${ className={`relative ${
replyTo.id ? "h-36 pt-16" : "h-24 pt-3" replyTo.id ? 'h-36 pt-16' : 'h-24 pt-3'
} w-full resize-none rounded-md px-5 !outline-none bg-zinc-800 placeholder:text-zinc-500`} } w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500`}
/> />
<div className="absolute right-2 bottom-0 h-11"> <div className="absolute bottom-0 right-2 h-11">
<div className="h-full flex gap-3 items-center justify-end text-zinc-500"> <div className="flex h-full items-center justify-end gap-3 text-zinc-500">
<MediaUploader setState={setValue} /> <MediaUploader setState={setValue} />
<button <button
type="button" type="button"

View File

@@ -1,12 +1,15 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from '@headlessui/react';
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { CancelIcon, HideIcon } from "@shared/icons"; import { Fragment, useContext, useState } from 'react';
import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip_dep"; import { CancelIcon, HideIcon } from '@shared/icons';
import { useChannelMessages } from "@stores/channels"; import { RelayContext } from '@shared/relayProvider';
import { dateToUnix } from "@utils/date"; import { Tooltip } from '@shared/tooltip_dep';
import { useAccount } from "@utils/hooks/useAccount";
import { Fragment, useContext, useState } from "react"; import { useChannelMessages } from '@stores/channels';
import { dateToUnix } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
export function MessageHideButton({ id }: { id: string }) { export function MessageHideButton({ id }: { id: string }) {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -30,11 +33,11 @@ export function MessageHideButton({ id }: { id: string }) {
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
// build event // build event
event.content = ""; event.content = '';
event.kind = 43; event.kind = 43;
event.created_at = dateToUnix(); event.created_at = dateToUnix();
event.pubkey = account.pubkey; event.pubkey = account.pubkey;
event.tags = [["e", id]]; event.tags = [['e', id]];
// publish event // publish event
event.publish(); event.publish();
@@ -95,11 +98,7 @@ export function MessageHideButton({ id }: { id: string }) {
onClick={closeModal} onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon <CancelIcon width={20} height={20} className="text-zinc-300" />
width={20}
height={20}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="leading-tight text-zinc-400"> <Dialog.Description className="leading-tight text-zinc-400">

View File

@@ -1,13 +1,15 @@
import { MessageHideButton } from "@app/channel/components/messages/hideButton"; import { MessageHideButton } from '@app/channel/components/messages/hideButton';
import { MessageMuteButton } from "@app/channel/components/messages/muteButton"; import { MessageMuteButton } from '@app/channel/components/messages/muteButton';
import { MessageReplyButton } from "@app/channel/components/messages/replyButton"; import { MessageReplyButton } from '@app/channel/components/messages/replyButton';
import { MentionNote } from "@shared/notes/mentions/note";
import { ImagePreview } from "@shared/notes/preview/image"; import { MentionNote } from '@shared/notes/mentions/note';
import { LinkPreview } from "@shared/notes/preview/link"; import { ImagePreview } from '@shared/notes/preview/image';
import { VideoPreview } from "@shared/notes/preview/video"; import { LinkPreview } from '@shared/notes/preview/link';
import { User } from "@shared/user"; import { VideoPreview } from '@shared/notes/preview/video';
import { parser } from "@utils/parser"; import { User } from '@shared/user';
import { LumeEvent } from "@utils/types";
import { parser } from '@utils/parser';
import { LumeEvent } from '@utils/types';
export function ChannelMessageItem({ data }: { data: LumeEvent }) { export function ChannelMessageItem({ data }: { data: LumeEvent }) {
const content = parser(data); const content = parser(data);
@@ -36,9 +38,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
<></> <></>
)} )}
{Array.isArray(content.notes) && content.notes.length ? ( {Array.isArray(content.notes) && content.notes.length ? (
content.notes.map((note: string) => ( content.notes.map((note: string) => <MentionNote key={note} id={note} />)
<MentionNote key={note} id={note} />
))
) : ( ) : (
<></> <></>
)} )}
@@ -46,11 +46,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
</div> </div>
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex"> <div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
<div className="inline-flex h-8 items-center justify-center gap-1.5 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800"> <div className="inline-flex h-8 items-center justify-center gap-1.5 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
<MessageReplyButton <MessageReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
id={data.id}
pubkey={data.pubkey}
content={data.content}
/>
<MessageHideButton id={data.id} /> <MessageHideButton id={data.id} />
<MessageMuteButton pubkey={data.pubkey} /> <MessageMuteButton pubkey={data.pubkey} />
</div> </div>

View File

@@ -1,12 +1,15 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from '@headlessui/react';
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { CancelIcon, MuteIcon } from "@shared/icons"; import { Fragment, useContext, useState } from 'react';
import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip_dep"; import { CancelIcon, MuteIcon } from '@shared/icons';
import { useChannelMessages } from "@stores/channels"; import { RelayContext } from '@shared/relayProvider';
import { dateToUnix } from "@utils/date"; import { Tooltip } from '@shared/tooltip_dep';
import { useAccount } from "@utils/hooks/useAccount";
import { Fragment, useContext, useState } from "react"; import { useChannelMessages } from '@stores/channels';
import { dateToUnix } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
export function MessageMuteButton({ pubkey }: { pubkey: string }) { export function MessageMuteButton({ pubkey }: { pubkey: string }) {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -30,11 +33,11 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
// build event // build event
event.content = ""; event.content = '';
event.kind = 44; event.kind = 44;
event.created_at = dateToUnix(); event.created_at = dateToUnix();
event.pubkey = account.pubkey; event.pubkey = account.pubkey;
event.tags = [["p", pubkey]]; event.tags = [['p', pubkey]];
// publish event // publish event
event.publish(); event.publish();
@@ -95,11 +98,7 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
onClick={closeModal} onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon <CancelIcon width={20} height={20} className="text-zinc-300" />
width={20}
height={20}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="leading-tight text-zinc-400"> <Dialog.Description className="leading-tight text-zinc-400">

View File

@@ -1,12 +1,17 @@
import { ReplyMessageIcon } from "@shared/icons"; import { ReplyMessageIcon } from '@shared/icons';
import { Tooltip } from "@shared/tooltip_dep"; import { Tooltip } from '@shared/tooltip_dep';
import { useChannelMessages } from "@stores/channels";
import { useChannelMessages } from '@stores/channels';
export function MessageReplyButton({ export function MessageReplyButton({
id, id,
pubkey, pubkey,
content, content,
}: { id: string; pubkey: string; content: string }) { }: {
id: string;
pubkey: string;
content: string;
}) {
const openReply = useChannelMessages((state: any) => state.openReply); const openReply = useChannelMessages((state: any) => state.openReply);
const createReply = () => { const createReply = () => {

View File

@@ -1,12 +1,10 @@
import { Image } from "@shared/image"; import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
export function ChannelMessageUserMute({ import { DEFAULT_AVATAR } from '@stores/constants';
pubkey,
}: { import { useProfile } from '@utils/hooks/useProfile';
pubkey: string;
}) { export function ChannelMessageUserMute({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey); const { user, isError, isLoading } = useProfile(pubkey);
return ( return (

View File

@@ -1,7 +1,9 @@
import { Image } from "@shared/image"; import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile"; import { DEFAULT_AVATAR } from '@stores/constants';
import { shortenKey } from "@utils/shortenKey";
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function UserReply({ pubkey }: { pubkey: string }) { export function UserReply({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey); const { user, isError, isLoading } = useProfile(pubkey);

View File

@@ -1,15 +1,18 @@
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile"; import { nip19 } from 'nostr-tools';
import { CopyIcon } from "@shared/icons";
import { Image } from "@shared/image"; import { useChannelProfile } from '@app/channel/hooks/useChannelProfile';
import { DEFAULT_AVATAR } from "@stores/constants";
import { nip19 } from "nostr-tools"; import { CopyIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
export function ChannelMetadata({ id }: { id: string }) { export function ChannelMetadata({ id }: { id: string }) {
const metadata = useChannelProfile(id); const metadata = useChannelProfile(id);
const noteID = id ? nip19.noteEncode(id) : null; const noteID = id ? nip19.noteEncode(id) : null;
const copyNoteID = async () => { const copyNoteID = async () => {
const { writeText } = await import("@tauri-apps/api/clipboard"); const { writeText } = await import('@tauri-apps/api/clipboard');
if (noteID) { if (noteID) {
await writeText(noteID); await writeText(noteID);
} }
@@ -17,19 +20,17 @@ export function ChannelMetadata({ id }: { id: string }) {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="relative shrink-0 rounded-md h-11 w-11"> <div className="relative h-11 w-11 shrink-0 rounded-md">
<Image <Image
src={metadata?.picture} src={metadata?.picture}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt={id} alt={id}
className="h-11 w-11 rounded-md object-contain bg-zinc-900" className="h-11 w-11 rounded-md bg-zinc-900 object-contain"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="inline-flex items-center gap-1"> <div className="inline-flex items-center gap-1">
<h5 className="leading-none text-lg font-semibold"> <h5 className="text-lg font-semibold leading-none">{metadata?.name}</h5>
{metadata?.name}
</h5>
<button type="button" onClick={() => copyNoteID()}> <button type="button" onClick={() => copyNoteID()}>
<CopyIcon width={14} height={14} className="text-zinc-400" /> <CopyIcon width={14} height={14} className="text-zinc-400" />
</button> </button>

View File

@@ -1,15 +1,18 @@
import { Image } from "@shared/image"; import { useState } from 'react';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile"; import { Image } from '@shared/image';
import { shortenKey } from "@utils/shortenKey";
import { useState } from "react"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function MutedItem({ data }: { data: any }) { export function MutedItem({ data }: { data: any }) {
const { user, isError, isLoading } = useProfile(data.content); const { user, isError, isLoading } = useProfile(data.content);
const [status, setStatus] = useState(data.status); const [status, setStatus] = useState(data.status);
const unmute = async () => { const unmute = async () => {
const { updateItemInBlacklist } = await import("@libs/storage"); const { updateItemInBlacklist } = await import('@libs/storage');
const res = await updateItemInBlacklist(data.content, 0); const res = await updateItemInBlacklist(data.content, 0);
if (res) { if (res) {
setStatus(0); setStatus(0);
@@ -17,7 +20,7 @@ export function MutedItem({ data }: { data: any }) {
}; };
const mute = async () => { const mute = async () => {
const { updateItemInBlacklist } = await import("@libs/storage"); const { updateItemInBlacklist } = await import('@libs/storage');
const res = await updateItemInBlacklist(data.content, 1); const res = await updateItemInBlacklist(data.content, 1);
if (res) { if (res) {
setStatus(1); setStatus(1);
@@ -49,7 +52,7 @@ export function MutedItem({ data }: { data: any }) {
</div> </div>
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start"> <div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
<span className="truncate text-base font-medium leading-none text-zinc-100"> <span className="truncate text-base font-medium leading-none text-zinc-100">
{user?.displayName || user?.name || "Pleb"} {user?.displayName || user?.name || 'Pleb'}
</span> </span>
<span className="text-base leading-none text-zinc-400"> <span className="text-base leading-none text-zinc-400">
{shortenKey(data.content)} {shortenKey(data.content)}

View File

@@ -1,11 +1,13 @@
import { getChannel, updateChannelMetadata } from "@libs/storage"; import { useQuery } from '@tanstack/react-query';
import { RelayContext } from "@shared/relayProvider"; import { useContext, useEffect } from 'react';
import { useQuery } from "@tanstack/react-query";
import { useContext, useEffect } from "react"; import { getChannel, updateChannelMetadata } from '@libs/storage';
import { RelayContext } from '@shared/relayProvider';
export function useChannelProfile(id: string) { export function useChannelProfile(id: string) {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
const { data } = useQuery(["channel-metadata", id], async () => { const { data } = useQuery(['channel-metadata', id], async () => {
return await getChannel(id); return await getChannel(id);
}); });
@@ -13,15 +15,15 @@ export function useChannelProfile(id: string) {
// subscribe to channel // subscribe to channel
const sub = ndk.subscribe( const sub = ndk.subscribe(
{ {
"#e": [id], '#e': [id],
kinds: [41], kinds: [41],
}, },
{ {
closeOnEose: true, closeOnEose: true,
}, }
); );
sub.addListener("event", (event: { content: string }) => { sub.addListener('event', (event: { content: string }) => {
// update in local database // update in local database
updateChannelMetadata(id, event.content); updateChannelMetadata(id, event.content);
}); });

View File

@@ -1,20 +1,19 @@
import { ChannelMessageItem } from "./components/messages/item"; import { useCallback, useContext, useEffect, useLayoutEffect, useRef } from 'react';
import { ChannelMembers } from "@app/channel/components/members"; import { useParams } from 'react-router-dom';
import { ChannelMessageForm } from "@app/channel/components/messages/form"; import { Virtuoso } from 'react-virtuoso';
import { ChannelMetadata } from "@app/channel/components/metadata";
import { RelayContext } from "@shared/relayProvider"; import { ChannelMembers } from '@app/channel/components/members';
import { useChannelMessages } from "@stores/channels"; import { ChannelMessageForm } from '@app/channel/components/messages/form';
import { dateToUnix, getHourAgo } from "@utils/date"; import { ChannelMetadata } from '@app/channel/components/metadata';
import { LumeEvent } from "@utils/types";
import { import { RelayContext } from '@shared/relayProvider';
useCallback,
useContext, import { useChannelMessages } from '@stores/channels';
useEffect,
useLayoutEffect, import { dateToUnix, getHourAgo } from '@utils/date';
useRef, import { LumeEvent } from '@utils/types';
} from "react";
import { useParams } from "react-router-dom"; import { ChannelMessageItem } from './components/messages/item';
import { Virtuoso } from "react-virtuoso";
const now = new Date(); const now = new Date();
@@ -25,11 +24,11 @@ const Header = (
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800"> <div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
{getHourAgo(24, now).toLocaleDateString("en-US", { {getHourAgo(24, now).toLocaleDateString('en-US', {
weekday: "long", weekday: 'long',
year: "numeric", year: 'numeric',
month: "long", month: 'long',
day: "numeric", day: 'numeric',
})} })}
</div> </div>
</div> </div>
@@ -53,13 +52,9 @@ export function ChannelScreen() {
const { id } = useParams(); const { id } = useParams();
const [messages, fetchMessages, addMessage, clearMessages] = const [messages, fetchMessages, addMessage, clearMessages] = useChannelMessages(
useChannelMessages((state: any) => [ (state: any) => [state.messages, state.fetch, state.add, state.clear]
state.messages, );
state.fetch,
state.add,
state.clear,
]);
useLayoutEffect(() => { useLayoutEffect(() => {
fetchMessages(id); fetchMessages(id);
@@ -69,14 +64,14 @@ export function ChannelScreen() {
// subscribe to channel // subscribe to channel
const sub = ndk.subscribe( const sub = ndk.subscribe(
{ {
"#e": [id], '#e': [id],
kinds: [42], kinds: [42],
since: dateToUnix(), since: dateToUnix(),
}, },
{ closeOnEose: false }, { closeOnEose: false }
); );
sub.addListener("event", (event: LumeEvent) => { sub.addListener('event', (event: LumeEvent) => {
addMessage(id, event); addMessage(id, event);
}); });
@@ -90,28 +85,28 @@ export function ChannelScreen() {
(index: string | number) => { (index: string | number) => {
return <ChannelMessageItem data={messages[index]} />; return <ChannelMessageItem data={messages[index]} />;
}, },
[messages], [messages]
); );
const computeItemKey = useCallback( const computeItemKey = useCallback(
(index: string | number) => { (index: string | number) => {
return messages[index].event_id; return messages[index].event_id;
}, },
[messages], [messages]
); );
return ( return (
<div className="h-full w-full grid grid-cols-3"> <div className="grid h-full w-full grid-cols-3">
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900"> <div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900" className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
> >
<h3 className="font-semibold text-zinc-100">Public Channel</h3> <h3 className="font-semibold text-zinc-100">Public Channel</h3>
</div> </div>
<div className="w-full h-full flex-1 p-3"> <div className="h-full w-full flex-1 p-3">
<div className="h-full flex flex-col justify-between rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden"> <div className="flex h-full flex-col justify-between overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex-1 w-full h-full"> <div className="h-full w-full flex-1">
{!messages ? ( {!messages ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
@@ -133,7 +128,7 @@ export function ChannelScreen() {
/> />
)} )}
</div> </div>
<div className="shrink-0 px-5 p-3 rounded-b-xl border-t border-zinc-800 bg-zinc-900 z-50"> <div className="z-50 shrink-0 rounded-b-xl border-t border-zinc-800 bg-zinc-900 p-3 px-5">
<ChannelMessageForm channelID={id} /> <ChannelMessageForm channelID={id} />
</div> </div>
</div> </div>
@@ -142,9 +137,9 @@ export function ChannelScreen() {
<div className="col-span-1 flex flex-col"> <div className="col-span-1 flex flex-col">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900" className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
/> />
<div className="p-3 flex flex-col gap-3"> <div className="flex flex-col gap-3 p-3">
<ChannelMetadata id={id} /> <ChannelMetadata id={id} />
<ChannelMembers id={id} /> <ChannelMembers id={id} />
</div> </div>

View File

@@ -1,14 +1,17 @@
import { Image } from "@shared/image"; import { NavLink } from 'react-router-dom';
import { DEFAULT_AVATAR } from "@stores/constants"; import { twMerge } from 'tailwind-merge';
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey"; import { Image } from '@shared/image';
import { NavLink } from "react-router-dom";
import { twMerge } from "tailwind-merge"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function ChatsListItem({ data }: { data: any }) { export function ChatsListItem({ data }: { data: any }) {
const { status, user } = useProfile(data.sender_pubkey); const { status, user } = useProfile(data.sender_pubkey);
if (status === "loading") { if (status === 'loading') {
return ( return (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
@@ -23,12 +26,12 @@ export function ChatsListItem({ data }: { data: any }) {
preventScrollReset={true} preventScrollReset={true}
className={({ isActive }) => className={({ isActive }) =>
twMerge( twMerge(
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5", 'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
isActive ? "bg-zinc-900/50 text-zinc-100" : "", isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
) )
} }
> >
<div className="inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<Image <Image
src={user?.image} src={user?.image}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
@@ -36,7 +39,7 @@ export function ChatsListItem({ data }: { data: any }) {
className="h-6 w-6 rounded object-cover" className="h-6 w-6 rounded object-cover"
/> />
</div> </div>
<div className="w-full inline-flex items-center justify-between"> <div className="inline-flex w-full items-center justify-between">
<div className="inline-flex items-baseline gap-1"> <div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[10rem] truncate font-medium text-zinc-200"> <h5 className="max-w-[10rem] truncate font-medium text-zinc-200">
{user?.nip05 || {user?.nip05 ||
@@ -47,7 +50,7 @@ export function ChatsListItem({ data }: { data: any }) {
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
{data.new_messages > 0 && ( {data.new_messages > 0 && (
<span className="inline-flex items-center justify-center rounded bg-fuchsia-400/10 w-8 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20"> <span className="inline-flex w-8 items-center justify-center rounded bg-fuchsia-400/10 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">
{data.new_messages} {data.new_messages}
</span> </span>
)} )}

View File

@@ -1,9 +1,12 @@
import { ChatsListItem } from "@app/chat/components/item"; import { useQuery } from '@tanstack/react-query';
import { NewMessageModal } from "@app/chat/components/modal";
import { ChatsListSelfItem } from "@app/chat/components/self"; import { ChatsListItem } from '@app/chat/components/item';
import { getChatsByPubkey } from "@libs/storage"; import { NewMessageModal } from '@app/chat/components/modal';
import { useQuery } from "@tanstack/react-query"; import { ChatsListSelfItem } from '@app/chat/components/self';
import { useAccount } from "@utils/hooks/useAccount";
import { getChatsByPubkey } from '@libs/storage';
import { useAccount } from '@utils/hooks/useAccount';
export function ChatsList() { export function ChatsList() {
const { account } = useAccount(); const { account } = useAccount();
@@ -13,29 +16,29 @@ export function ChatsList() {
data: chats, data: chats,
isFetching, isFetching,
} = useQuery( } = useQuery(
["chats"], ['chats'],
async () => { async () => {
const chats = await getChatsByPubkey(account.pubkey); const chats = await getChatsByPubkey(account.pubkey);
const sorted = chats.sort( const sorted = chats.sort(
(a, b) => parseInt(a.new_messages) - parseInt(b.new_messages), (a, b) => parseInt(a.new_messages) - parseInt(b.new_messages)
); );
return sorted; return sorted;
}, },
{ {
enabled: account ? true : false, enabled: account ? true : false,
}, }
); );
if (status === "loading") { if (status === 'loading') {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
</div> </div>
); );
@@ -49,7 +52,7 @@ export function ChatsList() {
) : ( ) : (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
)} )}
{chats.map((item) => { {chats.map((item) => {
@@ -60,7 +63,7 @@ export function ChatsList() {
{isFetching && ( {isFetching && (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
)} )}
</div> </div>

View File

@@ -1,15 +1,21 @@
import { usePublish } from "@libs/ndk"; import { nip04 } from 'nostr-tools';
import { EnterIcon } from "@shared/icons"; import { useCallback, useState } from 'react';
import { MediaUploader } from "@shared/mediaUploader";
import { nip04 } from "nostr-tools"; import { usePublish } from '@libs/ndk';
import { useCallback, useState } from "react";
import { EnterIcon } from '@shared/icons';
import { MediaUploader } from '@shared/mediaUploader';
export function ChatMessageForm({ export function ChatMessageForm({
receiverPubkey, receiverPubkey,
userPrivkey, userPrivkey,
}: { receiverPubkey: string; userPubkey: string; userPrivkey: string }) { }: {
receiverPubkey: string;
userPubkey: string;
userPrivkey: string;
}) {
const publish = usePublish(); const publish = usePublish();
const [value, setValue] = useState(""); const [value, setValue] = useState('');
const encryptMessage = useCallback(async () => { const encryptMessage = useCallback(async () => {
return await nip04.encrypt(userPrivkey, receiverPubkey, value); return await nip04.encrypt(userPrivkey, receiverPubkey, value);
@@ -17,13 +23,13 @@ export function ChatMessageForm({
const submit = async () => { const submit = async () => {
const message = await encryptMessage(); const message = await encryptMessage();
const tags = [["p", receiverPubkey]]; const tags = [['p', receiverPubkey]];
// publish message // publish message
await publish({ content: message, kind: 4, tags }); await publish({ content: message, kind: 4, tags });
// reset state // reset state
setValue(""); setValue('');
}; };
const handleEnterPress = (e: { const handleEnterPress = (e: {
@@ -31,7 +37,7 @@ export function ChatMessageForm({
shiftKey: any; shiftKey: any;
preventDefault: () => void; preventDefault: () => void;
}) => { }) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
submit(); submit();
} }
@@ -45,10 +51,10 @@ export function ChatMessageForm({
onKeyDown={handleEnterPress} onKeyDown={handleEnterPress}
spellCheck={false} spellCheck={false}
placeholder="Message" placeholder="Message"
className="relative h-11 w-full resize-none rounded-md px-5 !outline-none bg-zinc-800 placeholder:text-zinc-500" className="relative h-11 w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500"
/> />
<div className="absolute right-2 top-0 h-11"> <div className="absolute right-2 top-0 h-11">
<div className="h-full flex gap-3 items-center justify-end text-zinc-500"> <div className="flex h-full items-center justify-end gap-3 text-zinc-500">
<MediaUploader setState={setValue} /> <MediaUploader setState={setValue} />
<button <button
type="button" type="button"

View File

@@ -1,10 +1,12 @@
import { useDecryptMessage } from "@app/chat/hooks/useDecryptMessage"; import { useDecryptMessage } from '@app/chat/hooks/useDecryptMessage';
import { MentionNote } from "@shared/notes/mentions/note";
import { ImagePreview } from "@shared/notes/preview/image"; import { MentionNote } from '@shared/notes/mentions/note';
import { LinkPreview } from "@shared/notes/preview/link"; import { ImagePreview } from '@shared/notes/preview/image';
import { VideoPreview } from "@shared/notes/preview/video"; import { LinkPreview } from '@shared/notes/preview/link';
import { User } from "@shared/user"; import { VideoPreview } from '@shared/notes/preview/video';
import { parser } from "@utils/parser"; import { User } from '@shared/user';
import { parser } from '@utils/parser';
export function ChatMessageItem({ export function ChatMessageItem({
data, data,
@@ -18,7 +20,7 @@ export function ChatMessageItem({
const decryptedContent = useDecryptMessage(data, userPubkey, userPrivkey); const decryptedContent = useDecryptMessage(data, userPubkey, userPrivkey);
// if we have decrypted content, use it instead of the encrypted content // if we have decrypted content, use it instead of the encrypted content
if (decryptedContent) { if (decryptedContent) {
data["content"] = decryptedContent; data['content'] = decryptedContent;
} }
// parse the note content // parse the note content
const content = parser(data); const content = parser(data);
@@ -26,11 +28,7 @@ export function ChatMessageItem({
return ( return (
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20"> <div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
<div className="flex flex-col"> <div className="flex flex-col">
<User <User pubkey={data.sender_pubkey} time={data.created_at} isChat={true} />
pubkey={data.sender_pubkey}
time={data.created_at}
isChat={true}
/>
<div className="-mt-[20px] pl-[49px]"> <div className="-mt-[20px] pl-[49px]">
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100"> <p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
{content.parsed} {content.parsed}
@@ -39,9 +37,7 @@ export function ChatMessageItem({
{content.videos.length > 0 && <VideoPreview urls={content.videos} />} {content.videos.length > 0 && <VideoPreview urls={content.videos} />}
{content.links.length > 0 && <LinkPreview urls={content.links} />} {content.links.length > 0 && <LinkPreview urls={content.links} />}
{content.notes.length > 0 && {content.notes.length > 0 &&
content.notes.map((note: string) => ( content.notes.map((note: string) => <MentionNote key={note} id={note} />)}
<MentionNote key={note} id={note} />
))}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,12 @@
import { User } from "@app/auth/components/user"; import { Dialog, Transition } from '@headlessui/react';
import { Dialog, Transition } from "@headlessui/react"; import { Fragment, useState } from 'react';
import { CancelIcon, LoaderIcon, PlusIcon } from "@shared/icons"; import { useNavigate } from 'react-router-dom';
import { useAccount } from "@utils/hooks/useAccount";
import { Fragment, useState } from "react"; import { User } from '@app/auth/components/user';
import { useNavigate } from "react-router-dom";
import { CancelIcon, LoaderIcon, PlusIcon } from '@shared/icons';
import { useAccount } from '@utils/hooks/useAccount';
export function NewMessageModal() { export function NewMessageModal() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -77,22 +80,17 @@ export function NewMessageModal() {
onClick={closeModal} onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon <CancelIcon width={20} height={20} className="text-zinc-300" />
width={20}
height={20}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="text-sm leading-tight text-zinc-400"> <Dialog.Description className="text-sm leading-tight text-zinc-400">
All messages will be encrypted, but anyone can see who you All messages will be encrypted, but anyone can see who you chat
chat
</Dialog.Description> </Dialog.Description>
</div> </div>
</div> </div>
<div className="h-[500px] flex flex-col pb-5 overflow-x-hidden overflow-y-auto"> <div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-5">
{status === "loading" ? ( {status === 'loading' ? (
<div className="px-4 py-3 inline-flex items-center justify-center"> <div className="inline-flex items-center justify-center px-4 py-3">
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
</div> </div>
) : ( ) : (
@@ -106,7 +104,7 @@ export function NewMessageModal() {
<button <button
type="button" type="button"
onClick={() => openChat(follow)} onClick={() => openChat(follow)}
className="inline-flex text-sm w-max px-3 py-1.5 rounded border-t border-zinc-600/50 bg-zinc-700 hover:bg-fuchsia-500 transform translate-x-20 group-hover:translate-x-0 transition-transform ease-in-out duration-150" className="inline-flex w-max translate-x-20 transform rounded border-t border-zinc-600/50 bg-zinc-700 px-3 py-1.5 text-sm transition-transform duration-150 ease-in-out hover:bg-fuchsia-500 group-hover:translate-x-0"
> >
Chat Chat
</button> </button>

View File

@@ -1,14 +1,17 @@
import { Image } from "@shared/image"; import { NavLink } from 'react-router-dom';
import { DEFAULT_AVATAR } from "@stores/constants"; import { twMerge } from 'tailwind-merge';
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey"; import { Image } from '@shared/image';
import { NavLink } from "react-router-dom";
import { twMerge } from "tailwind-merge"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function ChatsListSelfItem({ data }: { data: any }) { export function ChatsListSelfItem({ data }: { data: any }) {
const { status, user } = useProfile(data.pubkey); const { status, user } = useProfile(data.pubkey);
if (status === "loading") { if (status === 'loading') {
return ( return (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
@@ -25,8 +28,8 @@ export function ChatsListSelfItem({ data }: { data: any }) {
preventScrollReset={true} preventScrollReset={true}
className={({ isActive }) => className={({ isActive }) =>
twMerge( twMerge(
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5", 'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
isActive ? "bg-zinc-900/50 text-zinc-100" : "", isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
) )
} }
> >

View File

@@ -1,8 +1,11 @@
import { Image } from "@shared/image"; import { Link } from 'react-router-dom';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile"; import { Image } from '@shared/image';
import { shortenKey } from "@utils/shortenKey";
import { Link } from "react-router-dom"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function ChatSidebar({ pubkey }: { pubkey: string }) { export function ChatSidebar({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
@@ -20,7 +23,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3 className="leading-none text-lg font-semibold"> <h3 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name} {user?.displayName || user?.name}
</h3> </h3>
<h5 className="leading-none text-zinc-400"> <h5 className="leading-none text-zinc-400">
@@ -31,7 +34,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
<p className="leading-tight">{user?.bio || user?.about}</p> <p className="leading-tight">{user?.bio || user?.about}</p>
<Link <Link
to={`/app/user/${pubkey}`} to={`/app/user/${pubkey}`}
className="mt-3 inline-flex w-full h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-zinc-800 text-sm text-zinc-300 hover:text-zinc-100 font-medium" className="mt-3 inline-flex h-10 w-full items-center justify-center rounded-md bg-zinc-900 text-sm font-medium text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100"
> >
View full profile View full profile
</Link> </Link>

View File

@@ -1,19 +1,13 @@
import { nip04 } from "nostr-tools"; import { nip04 } from 'nostr-tools';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
export function useDecryptMessage( export function useDecryptMessage(data: any, userPubkey: string, userPriv: string) {
data: any,
userPubkey: string,
userPriv: string,
) {
const [content, setContent] = useState(data.content); const [content, setContent] = useState(data.content);
useEffect(() => { useEffect(() => {
async function decrypt() { async function decrypt() {
const pubkey = const pubkey =
userPubkey === data.sender_pubkey userPubkey === data.sender_pubkey ? data.receiver_pubkey : data.sender_pubkey;
? data.receiver_pubkey
: data.sender_pubkey;
const result = await nip04.decrypt(userPriv, pubkey, data.content); const result = await nip04.decrypt(userPriv, pubkey, data.content);
setContent(result); setContent(result);
} }

View File

@@ -1,14 +1,18 @@
import { ChatMessageForm } from "@app/chat/components/messages/form"; import { NDKSubscription } from '@nostr-dev-kit/ndk';
import { ChatMessageItem } from "@app/chat/components/messages/item"; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ChatSidebar } from "@app/chat/components/sidebar"; import { useCallback, useContext, useEffect, useRef } from 'react';
import { createChat, getChatMessages } from "@libs/storage"; import { useParams } from 'react-router-dom';
import { NDKSubscription } from "@nostr-dev-kit/ndk"; import { Virtuoso } from 'react-virtuoso';
import { RelayContext } from "@shared/relayProvider";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ChatMessageForm } from '@app/chat/components/messages/form';
import { useAccount } from "@utils/hooks/useAccount"; import { ChatMessageItem } from '@app/chat/components/messages/item';
import { useCallback, useContext, useEffect, useRef } from "react"; import { ChatSidebar } from '@app/chat/components/sidebar';
import { useParams } from "react-router-dom";
import { Virtuoso } from "react-virtuoso"; import { createChat, getChatMessages } from '@libs/storage';
import { RelayContext } from '@shared/relayProvider';
import { useAccount } from '@utils/hooks/useAccount';
export function ChatScreen() { export function ChatScreen() {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -18,13 +22,13 @@ export function ChatScreen() {
const { pubkey } = useParams(); const { pubkey } = useParams();
const { account } = useAccount(); const { account } = useAccount();
const { status, data } = useQuery( const { status, data } = useQuery(
["chat", pubkey], ['chat', pubkey],
async () => { async () => {
return await getChatMessages(account.pubkey, pubkey); return await getChatMessages(account.pubkey, pubkey);
}, },
{ {
enabled: account ? true : false, enabled: account ? true : false,
}, }
); );
const itemContent: any = useCallback( const itemContent: any = useCallback(
@@ -37,14 +41,14 @@ export function ChatScreen() {
/> />
); );
}, },
[data], [data]
); );
const computeItemKey = useCallback( const computeItemKey = useCallback(
(index: string | number) => { (index: string | number) => {
return data[index].id; return data[index].id;
}, },
[data], [data]
); );
const chat = useMutation({ const chat = useMutation({
@@ -55,11 +59,11 @@ export function ChatScreen() {
data.sender_pubkey, data.sender_pubkey,
data.content, data.content,
data.tags, data.tags,
data.created_at, data.created_at
); );
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["chat", pubkey] }); queryClient.invalidateQueries({ queryKey: ['chat', pubkey] });
}, },
}); });
@@ -68,15 +72,15 @@ export function ChatScreen() {
{ {
kinds: [4], kinds: [4],
authors: [account.pubkey], authors: [account.pubkey],
"#p": [pubkey], '#p': [pubkey],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
}, },
{ {
closeOnEose: false, closeOnEose: false,
}, }
); );
sub.addListener("event", (event) => { sub.addListener('event', (event) => {
chat.mutate({ chat.mutate({
id: event.id, id: event.id,
receiver_pubkey: pubkey, receiver_pubkey: pubkey,
@@ -93,18 +97,18 @@ export function ChatScreen() {
}, [pubkey]); }, [pubkey]);
return ( return (
<div className="h-full w-full grid grid-cols-3"> <div className="grid h-full w-full grid-cols-3">
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900"> <div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900" className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
> >
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3> <h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
</div> </div>
<div className="w-full h-full flex-1 p-3"> <div className="h-full w-full flex-1 p-3">
<div className="h-full flex flex-col justify-between rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden"> <div className="flex h-full flex-col justify-between overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex-1 w-full h-full"> <div className="h-full w-full flex-1">
{status === "loading" ? ( {status === 'loading' ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
<Virtuoso <Virtuoso
@@ -117,14 +121,14 @@ export function ChatScreen() {
followOutput={true} followOutput={true}
overscan={50} overscan={50}
increaseViewportBy={{ top: 200, bottom: 200 }} increaseViewportBy={{ top: 200, bottom: 200 }}
className="relative scrollbar-hide overflow-y-auto" className="scrollbar-hide relative overflow-y-auto"
components={{ components={{
EmptyPlaceholder: () => Empty, EmptyPlaceholder: () => Empty,
}} }}
/> />
)} )}
</div> </div>
<div className="shrink-0 px-5 p-3 rounded-b-xl border-t border-zinc-800 bg-zinc-900 z-50"> <div className="z-50 shrink-0 rounded-b-xl border-t border-zinc-800 bg-zinc-900 p-3 px-5">
<ChatMessageForm <ChatMessageForm
receiverPubkey={pubkey} receiverPubkey={pubkey}
userPubkey={account.pubkey} userPubkey={account.pubkey}
@@ -137,7 +141,7 @@ export function ChatScreen() {
<div className="col-span-1"> <div className="col-span-1">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900" className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
/> />
<ChatSidebar pubkey={pubkey} /> <ChatSidebar pubkey={pubkey} />
</div> </div>
@@ -146,10 +150,10 @@ export function ChatScreen() {
} }
const Empty = ( const Empty = (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full flex flex-col gap-1 text-center"> <div className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 transform flex-col gap-1 text-center">
<h3 className="mb-2 text-4xl">🙌</h3> <h3 className="mb-2 text-4xl">🙌</h3>
<p className="leading-none text-zinc-400"> <p className="leading-none text-zinc-400">
You two didn't talk yet, let's send first message You two didn&apos;t talk yet, let&apos;s send first message
</p> </p>
</div> </div>
); );

View File

@@ -1,10 +1,10 @@
import { useRouteError } from "react-router-dom"; import { useRouteError } from 'react-router-dom';
export function ErrorScreen() { export function ErrorScreen() {
const error: any = useRouteError(); const error: any = useRouteError();
return ( return (
<div className="w-full h-full flex items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div> <div>
<h1>Oops!</h1> <h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p> <p>Sorry, an unexpected error has occurred.</p>

View File

@@ -1,4 +1,8 @@
import { prefetchEvents } from "@libs/ndk"; import { NDKFilter } from '@nostr-dev-kit/ndk';
import { useContext, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { prefetchEvents } from '@libs/ndk';
import { import {
countTotalNotes, countTotalNotes,
createChannelMessage, createChannelMessage,
@@ -7,14 +11,13 @@ import {
getChannels, getChannels,
getLastLogin, getLastLogin,
updateLastLogin, updateLastLogin,
} from "@libs/storage"; } from '@libs/storage';
import { NDKFilter } from "@nostr-dev-kit/ndk";
import { LoaderIcon, LumeIcon } from "@shared/icons"; import { LoaderIcon, LumeIcon } from '@shared/icons';
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from '@shared/relayProvider';
import { dateToUnix, getHourAgo } from "@utils/date";
import { useAccount } from "@utils/hooks/useAccount"; import { dateToUnix, getHourAgo } from '@utils/date';
import { useContext, useEffect, useRef } from "react"; import { useAccount } from '@utils/hooks/useAccount';
import { useNavigate } from "react-router-dom";
const totalNotes = await countTotalNotes(); const totalNotes = await countTotalNotes();
const lastLogin = await getLastLogin(); const lastLogin = await getLastLogin();
@@ -51,13 +54,13 @@ export function Root() {
event.kind, event.kind,
event.tags, event.tags,
event.content, event.content,
event.created_at, event.created_at
); );
}); });
return true; return true;
} catch (e) { } catch (e) {
console.log("error: ", e); console.log('error: ', e);
} }
} }
@@ -70,7 +73,7 @@ export function Root() {
}; };
const receiveFilter: NDKFilter = { const receiveFilter: NDKFilter = {
kinds: [4], kinds: [4],
"#p": [account.pubkey], '#p': [account.pubkey],
since: lastLogin, since: lastLogin,
}; };
@@ -79,24 +82,24 @@ export function Root() {
const events = [...sendMessages, ...receiveMessages]; const events = [...sendMessages, ...receiveMessages];
events.forEach((event) => { events.forEach((event) => {
const receiverPubkey = const receiverPubkey = event.tags.find((t) => t[0] === 'p')[1] || account.pubkey;
event.tags.find((t) => t[0] === "p")[1] || account.pubkey;
createChat( createChat(
event.id, event.id,
receiverPubkey, receiverPubkey,
event.pubkey, event.pubkey,
event.content, event.content,
event.tags, event.tags,
event.created_at, event.created_at
); );
}); });
return true; return true;
} catch (e) { } catch (e) {
console.log("error: ", e); console.log('error: ', e);
} }
} }
/*
async function fetchChannelMessages() { async function fetchChannelMessages() {
try { try {
const ids = []; const ids = [];
@@ -105,11 +108,10 @@ export function Root() {
ids.push(channel.event_id); ids.push(channel.event_id);
}); });
const since = const since = lastLogin === 0 ? dateToUnix(getHourAgo(48, now.current)) : lastLogin;
lastLogin === 0 ? dateToUnix(getHourAgo(48, now.current)) : lastLogin;
const filter: NDKFilter = { const filter: NDKFilter = {
"#e": ids, '#e': ids,
kinds: [42], kinds: [42],
since: since, since: since,
}; };
@@ -125,16 +127,17 @@ export function Root() {
event.kind, event.kind,
event.content, event.content,
event.tags, event.tags,
event.created_at, event.created_at
); );
} }
}); });
return true; return true;
} catch (e) { } catch (e) {
console.log("error: ", e); console.log('error: ', e);
} }
} }
*/
useEffect(() => { useEffect(() => {
async function prefetch() { async function prefetch() {
@@ -145,12 +148,12 @@ export function Root() {
if (chats) { if (chats) {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
await updateLastLogin(now); await updateLastLogin(now);
navigate("/app/space", { replace: true }); navigate('/app/space', { replace: true });
} }
} }
} }
if (status === "success" && account) { if (status === 'success' && account) {
prefetch(); prefetch();
} }
}, [status]); }, [status]);
@@ -170,8 +173,7 @@ export function Root() {
Here&apos;s an interesting fact: Here&apos;s an interesting fact:
</h3> </h3>
<p className="font-medium text-zinc-300 dark:text-zinc-600"> <p className="font-medium text-zinc-300 dark:text-zinc-600">
Bitcoin and Nostr can be used by anyone, and no one can stop Bitcoin and Nostr can be used by anyone, and no one can stop you!
you!
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,50 +1,55 @@
import { EyeOffIcon, EyeOnIcon } from "@shared/icons"; import { useState } from 'react';
import { useAccount } from "@utils/hooks/useAccount";
import { useState } from "react"; import { EyeOffIcon, EyeOnIcon } from '@shared/icons';
import { useAccount } from '@utils/hooks/useAccount';
export function AccountSettingsScreen() { export function AccountSettingsScreen() {
const { status, account } = useAccount(); const { status, account } = useAccount();
const [type, setType] = useState("password"); const [type, setType] = useState('password');
const showPrivateKey = () => { const showPrivateKey = () => {
if (type === "password") { if (type === 'password') {
setType("text"); setType('text');
} else { } else {
setType("password"); setType('password');
} }
}; };
return ( return (
<div className="w-full h-full px-3 pt-12"> <div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h1 className="text-lg font-semibold text-zinc-100">Account</h1> <h1 className="text-lg font-semibold text-zinc-100">Account</h1>
<div className=""> <div className="">
{status === "loading" ? ( {status === 'loading' ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-base font-semibold text-zinc-400"> <label htmlFor="pubkey" className="text-base font-semibold text-zinc-400">
Public Key Public Key
</label> </label>
<input <input
readOnly readOnly
value={account.pubkey} value={account.pubkey}
className="relative w-2/3 rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100" className="relative w-2/3 rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-base font-semibold text-zinc-400"> <label htmlFor="npub" className="text-base font-semibold text-zinc-400">
Npub Npub
</label> </label>
<input <input
readOnly readOnly
value={account.npub} value={account.npub}
className="relative w-2/3 rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100" className="relative w-2/3 rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-base font-semibold text-zinc-400"> <label
htmlFor="privkey"
className="text-base font-semibold text-zinc-400"
>
Private Key Private Key
</label> </label>
<div className="relative w-2/3"> <div className="relative w-2/3">
@@ -52,14 +57,14 @@ export function AccountSettingsScreen() {
readOnly readOnly
type={type} type={type}
value={account.privkey} value={account.privkey}
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100" className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/> />
<button <button
type="button" type="button"
onClick={() => showPrivateKey()} onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700" className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
> >
{type === "password" ? ( {type === 'password' ? (
<EyeOffIcon <EyeOffIcon
width={20} width={20}
height={20} height={20}

View File

@@ -1,8 +1,9 @@
import { Switch } from "@headlessui/react"; import { Switch } from '@headlessui/react';
import { getSetting, updateSetting } from "@libs/storage"; import { useEffect, useState } from 'react';
import { useEffect, useState } from "react"; import { twMerge } from 'tailwind-merge';
import { twMerge } from "tailwind-merge"; import { disable, enable, isEnabled } from 'tauri-plugin-autostart-api';
import { disable, enable, isEnabled } from "tauri-plugin-autostart-api";
import { getSetting, updateSetting } from '@libs/storage';
export function AutoStartSetting() { export function AutoStartSetting() {
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
@@ -10,18 +11,18 @@ export function AutoStartSetting() {
const toggle = async () => { const toggle = async () => {
if (!enabled) { if (!enabled) {
await enable(); await enable();
await updateSetting("auto_start", 1); await updateSetting('auto_start', 1);
console.log(`registered for autostart? ${await isEnabled()}`); console.log(`registered for autostart? ${await isEnabled()}`);
} else { } else {
await disable(); await disable();
await updateSetting("auto_start", 0); await updateSetting('auto_start', 0);
} }
setEnabled(!enabled); setEnabled(!enabled);
}; };
useEffect(() => { useEffect(() => {
async function getAppSetting() { async function getAppSetting() {
const setting = await getSetting("auto_start"); const setting = await getSetting('auto_start');
if (parseInt(setting) === 0) { if (parseInt(setting) === 0) {
setEnabled(false); setEnabled(false);
} else { } else {
@@ -32,27 +33,23 @@ export function AutoStartSetting() {
}, []); }, []);
return ( return (
<div className="px-5 py-4 inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="leading-none font-medium text-zinc-200"> <span className="font-medium leading-none text-zinc-200">Auto start</span>
Auto start <span className="text-sm leading-none text-zinc-400">Auto start at login</span>
</span>
<span className="leading-none text-sm text-zinc-400">
Auto start at login
</span>
</div> </div>
<Switch <Switch
checked={enabled} checked={enabled}
onChange={toggle} onChange={toggle}
className={twMerge( className={twMerge(
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2", 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2',
enabled ? "bg-fuchsia-500" : "bg-zinc-700", enabled ? 'bg-fuchsia-500' : 'bg-zinc-700'
)} )}
> >
<span <span
className={twMerge( className={twMerge(
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-zinc-900 shadow ring-0 transition duration-200 ease-in-out", 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-zinc-900 shadow ring-0 transition duration-200 ease-in-out',
enabled ? "translate-x-5" : "translate-x-0", enabled ? 'translate-x-5' : 'translate-x-0'
)} )}
/> />
</Switch> </Switch>

View File

@@ -1,24 +1,24 @@
import { getSetting, updateSetting } from "@libs/storage"; import { useState } from 'react';
import { CheckCircleIcon } from "@shared/icons";
import { useState } from "react";
const setting = await getSetting("cache_time"); import { getSetting, updateSetting } from '@libs/storage';
import { CheckCircleIcon } from '@shared/icons';
const setting = await getSetting('cache_time');
const cacheTime = setting; const cacheTime = setting;
export function CacheTimeSetting() { export function CacheTimeSetting() {
const [time, setTime] = useState(cacheTime); const [time, setTime] = useState(cacheTime);
const update = async () => { const update = async () => {
await updateSetting("cache_time", time); await updateSetting('cache_time', time);
}; };
return ( return (
<div className="px-5 py-4 inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="leading-none font-medium text-zinc-200"> <span className="font-medium leading-none text-zinc-200">Cache time</span>
Cache time <span className="text-sm leading-none text-zinc-400">
</span>
<span className="leading-none text-sm text-zinc-400">
The length of time before inactive data gets removed from the cache The length of time before inactive data gets removed from the cache
</span> </span>
</div> </div>
@@ -28,14 +28,14 @@ export function CacheTimeSetting() {
onChange={(e) => setTime(e.currentTarget.value)} onChange={(e) => setTime(e.currentTarget.value)}
autoCapitalize="none" autoCapitalize="none"
autoCorrect="none" autoCorrect="none"
className="w-24 h-8 rounded-md px-2 bg-zinc-800 text-zinc-300 text-right font-medium focus:outline-none" className="h-8 w-24 rounded-md bg-zinc-800 px-2 text-right font-medium text-zinc-300 focus:outline-none"
/> />
<button <button
type="button" type="button"
onClick={() => update()} onClick={() => update()}
className="w-8 h-8 inline-flex items-center justify-center font-medium bg-zinc-800 hover:bg-fuchsia-500 rounded-md" className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500"
> >
<CheckCircleIcon className="w-4 h-4 text-zinc-100" /> <CheckCircleIcon className="h-4 w-4 text-zinc-100" />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,24 +1,25 @@
import { RefreshIcon } from "@shared/icons"; import { getVersion } from '@tauri-apps/api/app';
import { getVersion } from "@tauri-apps/api/app";
import { RefreshIcon } from '@shared/icons';
const appVersion = await getVersion(); const appVersion = await getVersion();
export function VersionSetting() { export function VersionSetting() {
return ( return (
<div className="px-5 py-4 inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="leading-none font-medium text-zinc-200">Version</span> <span className="font-medium leading-none text-zinc-200">Version</span>
<span className="leading-none text-sm text-zinc-400"> <span className="text-sm leading-none text-zinc-400">
You're using latest version You&apos;re using latest version
</span> </span>
</div> </div>
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<span className="text-zinc-300 font-medium">{appVersion}</span> <span className="font-medium text-zinc-300">{appVersion}</span>
<button <button
type="button" type="button"
className="w-8 h-8 inline-flex items-center justify-center font-medium bg-zinc-800 hover:bg-fuchsia-500 rounded-md" className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500"
> >
<RefreshIcon className="w-4 h-4 text-zinc-100" /> <RefreshIcon className="h-4 w-4 text-zinc-100" />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,14 +1,14 @@
import { AutoStartSetting } from "@app/settings/components/autoStart"; import { AutoStartSetting } from '@app/settings/components/autoStart';
import { CacheTimeSetting } from "@app/settings/components/cacheTime"; import { CacheTimeSetting } from '@app/settings/components/cacheTime';
import { VersionSetting } from "@app/settings/components/version"; import { VersionSetting } from '@app/settings/components/version';
export function GeneralSettingsScreen() { export function GeneralSettingsScreen() {
return ( return (
<div className="w-full h-full px-3 pt-12"> <div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h1 className="text-lg font-semibold text-zinc-100">General</h1> <h1 className="text-lg font-semibold text-zinc-100">General</h1>
<div className="w-full bg-zinc-900 border-t border-zinc-800/50 rounded-xl"> <div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="w-full h-full flex flex-col divide-y divide-zinc-800"> <div className="flex h-full w-full flex-col divide-y divide-zinc-800">
<AutoStartSetting /> <AutoStartSetting />
<CacheTimeSetting /> <CacheTimeSetting />
<VersionSetting /> <VersionSetting />

View File

@@ -1,104 +1,84 @@
import { CommandIcon } from "@shared/icons"; import { CommandIcon } from '@shared/icons';
export function ShortcutsSettingsScreen() { export function ShortcutsSettingsScreen() {
return ( return (
<div className="w-full h-full px-3 pt-12"> <div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h1 className="text-lg font-semibold text-zinc-100">Shortcuts</h1> <h1 className="text-lg font-semibold text-zinc-100">Shortcuts</h1>
<div className="w-full bg-zinc-900 border-t border-zinc-800/50 rounded-xl"> <div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="w-full h-full flex flex-col divide-y divide-zinc-800"> <div className="flex h-full w-full flex-col divide-y divide-zinc-800">
<div className="px-5 py-4 inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="leading-none font-medium text-zinc-200"> <span className="font-medium leading-none text-zinc-200">
Open composer Open composer
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon <CommandIcon width={12} height={12} className="text-zinc-500" />
width={12}
height={12}
className="text-zinc-500"
/>
</div> </div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-zinc-500 text-sm leading-none">N</span> <span className="text-sm leading-none text-zinc-500">N</span>
</div> </div>
</div> </div>
</div> </div>
<div className="px-5 py-4 inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="leading-none font-medium text-zinc-200"> <span className="font-medium leading-none text-zinc-200">
Add image block Add image block
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon <CommandIcon width={12} height={12} className="text-zinc-500" />
width={12}
height={12}
className="text-zinc-500"
/>
</div> </div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-zinc-500 text-sm leading-none">I</span> <span className="text-sm leading-none text-zinc-500">I</span>
</div> </div>
</div> </div>
</div> </div>
<div className="px-5 py-4 inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="leading-none font-medium text-zinc-200"> <span className="font-medium leading-none text-zinc-200">
Add newsfeed block Add newsfeed block
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon <CommandIcon width={12} height={12} className="text-zinc-500" />
width={12}
height={12}
className="text-zinc-500"
/>
</div> </div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-zinc-500 text-sm leading-none">F</span> <span className="text-sm leading-none text-zinc-500">F</span>
</div> </div>
</div> </div>
</div> </div>
<div className="px-5 py-4 inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="leading-none font-medium text-zinc-200"> <span className="font-medium leading-none text-zinc-200">
Open personal page Open personal page
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon <CommandIcon width={12} height={12} className="text-zinc-500" />
width={12}
height={12}
className="text-zinc-500"
/>
</div> </div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-zinc-500 text-sm leading-none">P</span> <span className="text-sm leading-none text-zinc-500">P</span>
</div> </div>
</div> </div>
</div> </div>
<div className="px-5 py-4 inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="leading-none font-medium text-zinc-200"> <span className="font-medium leading-none text-zinc-200">
Open notification Open notification
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon <CommandIcon width={12} height={12} className="text-zinc-500" />
width={12}
height={12}
className="text-zinc-500"
/>
</div> </div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-zinc-500 text-sm leading-none">B</span> <span className="text-sm leading-none text-zinc-500">B</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { AddFeedBlock } from "@app/space/components/addFeed"; import { AddFeedBlock } from '@app/space/components/addFeed';
import { AddImageBlock } from "@app/space/components/addImage"; import { AddImageBlock } from '@app/space/components/addImage';
export function AddBlock() { export function AddBlock() {
return ( return (

View File

@@ -1,16 +1,21 @@
import { User } from "@app/auth/components/user"; import { Dialog, Transition } from '@headlessui/react';
import { Dialog, Transition } from "@headlessui/react"; import { Combobox } from '@headlessui/react';
import { Combobox } from "@headlessui/react"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createBlock } from "@libs/storage"; import { nip19 } from 'nostr-tools';
import { CancelIcon, CheckCircleIcon, CommandIcon } from "@shared/icons"; import { Fragment, useState } from 'react';
import { DEFAULT_AVATAR } from "@stores/constants"; import { useForm } from 'react-hook-form';
import { ADD_FEEDBLOCK_SHORTCUT } from "@stores/shortcuts"; import { useHotkeys } from 'react-hotkeys-hook';
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAccount } from "@utils/hooks/useAccount"; import { User } from '@app/auth/components/user';
import { nip19 } from "nostr-tools";
import { Fragment, useEffect, useState } from "react"; import { createBlock } from '@libs/storage';
import { useForm } from "react-hook-form";
import { useHotkeys } from "react-hotkeys-hook"; import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
import { DEFAULT_AVATAR } from '@stores/constants';
import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts';
import { useAccount } from '@utils/hooks/useAccount';
export function AddFeedBlock() { export function AddFeedBlock() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -18,7 +23,7 @@ export function AddFeedBlock() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const [query, setQuery] = useState(""); const [query, setQuery] = useState('');
const { status, account } = useAccount(); const { status, account } = useAccount();
@@ -37,7 +42,7 @@ export function AddFeedBlock() {
return createBlock(data.kind, data.title, data.content); return createBlock(data.kind, data.title, data.content);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["blocks"] }); queryClient.invalidateQueries({ queryKey: ['blocks'] });
}, },
}); });
@@ -52,7 +57,7 @@ export function AddFeedBlock() {
setLoading(true); setLoading(true);
selected.forEach((item, index) => { selected.forEach((item, index) => {
if (item.substring(0, 4) === "npub") { if (item.substring(0, 4) === 'npub') {
selected[index] = nip19.decode(item).data; selected[index] = nip19.decode(item).data;
} }
}); });
@@ -76,14 +81,14 @@ export function AddFeedBlock() {
<button <button
type="button" type="button"
onClick={() => openModal()} onClick={() => openModal()}
className="inline-flex w-56 h-9 items-center justify-start gap-2.5 rounded-md px-2.5" className="inline-flex h-9 w-56 items-center justify-start gap-2.5 rounded-md px-2.5"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<CommandIcon width={12} height={12} className="text-zinc-500" /> <CommandIcon width={12} height={12} className="text-zinc-500" />
</div> </div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<span className="text-zinc-500 text-sm leading-none">F</span> <span className="text-sm leading-none text-zinc-500">F</span>
</div> </div>
</div> </div>
<div> <div>
@@ -128,60 +133,53 @@ export function AddFeedBlock() {
onClick={closeModal} onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon <CancelIcon width={14} height={14} className="text-zinc-300" />
width={14}
height={14}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="text-sm leading-tight text-zinc-400"> <Dialog.Description className="text-sm leading-tight text-zinc-400">
Specific newsfeed space for people you want to keep up to Specific newsfeed space for people you want to keep up to date
date
</Dialog.Description> </Dialog.Description>
</div> </div>
</div> </div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3"> <div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0" className="mb-0 flex h-full w-full flex-col gap-4"
> >
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400"> <label
htmlFor="title"
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
>
Title * Title *
</label> </label>
<input <input
type={"text"} type={'text'}
{...register("title", { {...register('title', {
required: true, required: true,
})} })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-md px-3 py-2 !outline-none placeholder:text-zinc-500 bg-zinc-800 text-zinc-100" className="relative h-10 w-full rounded-md bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400"> <span className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Choose at least 1 user * Choose at least 1 user *
</label> </span>
<div className="w-full h-[300px] flex flex-col rounded-lg border-t border-zinc-700/50 bg-zinc-800 overflow-x-hidden overflow-y-auto"> <div className="flex h-[300px] w-full flex-col overflow-y-auto overflow-x-hidden rounded-lg border-t border-zinc-700/50 bg-zinc-800">
<div className="w-full px-3 py-2"> <div className="w-full px-3 py-2">
<Combobox <Combobox value={selected} onChange={setSelected} multiple>
value={selected}
onChange={setSelected}
multiple
>
<Combobox.Input <Combobox.Input
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
spellCheck={false} spellCheck={false}
autoFocus={false}
placeholder="Enter pubkey or npub..." placeholder="Enter pubkey or npub..."
className="mb-2 relative h-10 w-full rounded-md px-3 py-2 !outline-none placeholder:text-zinc-500 bg-zinc-700 text-zinc-100" className="relative mb-2 h-10 w-full rounded-md bg-zinc-700 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
<Combobox.Options static> <Combobox.Options static>
{query.length > 0 && ( {query.length > 0 && (
<Combobox.Option <Combobox.Option
value={query} value={query}
className="group w-full flex items-center justify-between px-2 py-2 rounded-md hover:bg-zinc-700" className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-zinc-700"
> >
{({ selected }) => ( {({ selected }) => (
<> <>
@@ -189,7 +187,7 @@ export function AddFeedBlock() {
<img <img
alt={query} alt={query}
src={DEFAULT_AVATAR} src={DEFAULT_AVATAR}
className="w-11 h-11 shrink-0 object-cover rounded" className="h-11 w-11 shrink-0 rounded object-cover"
/> />
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<span className="text-base leading-tight text-zinc-400"> <span className="text-base leading-tight text-zinc-400">
@@ -198,26 +196,26 @@ export function AddFeedBlock() {
</div> </div>
</div> </div>
{selected && ( {selected && (
<CheckCircleIcon className="w-4 h-4 text-green-500" /> <CheckCircleIcon className="h-4 w-4 text-green-500" />
)} )}
</> </>
)} )}
</Combobox.Option> </Combobox.Option>
)} )}
{status === "loading" ? ( {status === 'loading' ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
JSON.parse(account.follows).map((follow) => ( JSON.parse(account.follows).map((follow) => (
<Combobox.Option <Combobox.Option
key={follow} key={follow}
value={follow} value={follow}
className="group w-full flex items-center justify-between px-2 py-2 rounded-md hover:bg-zinc-700" className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-zinc-700"
> >
{({ selected }) => ( {({ selected }) => (
<> <>
<User pubkey={follow} /> <User pubkey={follow} />
{selected && ( {selected && (
<CheckCircleIcon className="w-4 h-4 text-green-500" /> <CheckCircleIcon className="h-4 w-4 text-green-500" />
)} )}
</> </>
)} )}
@@ -233,32 +231,12 @@ export function AddFeedBlock() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30" className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
> >
{loading ? ( {loading ? (
<svg <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<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"
/>
</svg>
) : ( ) : (
"Confirm" 'Confirm'
)} )}
</button> </button>
</div> </div>

View File

@@ -1,20 +1,24 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from '@headlessui/react';
import { createBlock } from "@libs/storage"; import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { CancelIcon, CommandIcon } from "@shared/icons"; import { open } from '@tauri-apps/api/dialog';
import { Image } from "@shared/image"; import { Body, fetch } from '@tauri-apps/api/http';
import { RelayContext } from "@shared/relayProvider"; import { Fragment, useContext, useEffect, useRef, useState } from 'react';
import { DEFAULT_AVATAR } from "@stores/constants"; import { useForm } from 'react-hook-form';
import { ADD_IMAGEBLOCK_SHORTCUT } from "@stores/shortcuts"; import { useHotkeys } from 'react-hotkeys-hook';
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { open } from "@tauri-apps/api/dialog"; import { createBlock } from '@libs/storage';
import { Body, fetch } from "@tauri-apps/api/http";
import { createBlobFromFile } from "@utils/createBlobFromFile"; import { CancelIcon, CommandIcon } from '@shared/icons';
import { dateToUnix } from "@utils/date"; import { Image } from '@shared/image';
import { useAccount } from "@utils/hooks/useAccount"; import { RelayContext } from '@shared/relayProvider';
import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useHotkeys } from "react-hotkeys-hook"; import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
import { createBlobFromFile } from '@utils/createBlobFromFile';
import { dateToUnix } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
export function AddImageBlock() { export function AddImageBlock() {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -22,7 +26,7 @@ export function AddImageBlock() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState(""); const [image, setImage] = useState('');
const { account } = useAccount(); const { account } = useAccount();
@@ -51,8 +55,8 @@ export function AddImageBlock() {
multiple: false, multiple: false,
filters: [ filters: [
{ {
name: "Image", name: 'Image',
extensions: ["png", "jpeg", "jpg"], extensions: ['png', 'jpeg', 'jpg'],
}, },
], ],
}); });
@@ -62,19 +66,19 @@ export function AddImageBlock() {
} else if (selected === null) { } else if (selected === null) {
// user cancelled the selection // user cancelled the selection
} else { } else {
const filename = selected.split("/").pop(); const filename = selected.split('/').pop();
const file = await createBlobFromFile(selected); const file = await createBlobFromFile(selected);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
const res: any = await fetch("https://void.cat/upload?cli=false", { const res: any = await fetch('https://void.cat/upload?cli=false', {
method: "POST", method: 'POST',
timeout: 5, timeout: 5,
headers: { headers: {
accept: "*/*", accept: '*/*',
"Content-Type": "application/octet-stream", 'Content-Type': 'application/octet-stream',
"V-Filename": filename, 'V-Filename': filename,
"V-Description": "Upload from https://lume.nu", 'V-Description': 'Upload from https://lume.nu',
"V-Strip-Metadata": "true", 'V-Strip-Metadata': 'true',
}, },
body: Body.bytes(buf), body: Body.bytes(buf),
}); });
@@ -82,11 +86,11 @@ export function AddImageBlock() {
if (res.ok) { if (res.ok) {
const imageURL = `https://void.cat/d/${res.data.file.id}.webp`; const imageURL = `https://void.cat/d/${res.data.file.id}.webp`;
tags.current = [ tags.current = [
["url", imageURL], ['url', imageURL],
["m", res.data.file.metadata.mimeType], ['m', res.data.file.metadata.mimeType],
["x", res.data.file.metadata.digest], ['x', res.data.file.metadata.digest],
["size", res.data.file.metadata.size], ['size', res.data.file.metadata.size],
["magnet", res.data.file.metadata.magnetLink], ['magnet', res.data.file.metadata.magnetLink],
]; ];
setImage(imageURL); setImage(imageURL);
@@ -99,7 +103,7 @@ export function AddImageBlock() {
return createBlock(data.kind, data.title, data.content); return createBlock(data.kind, data.title, data.content);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["blocks"] }); queryClient.invalidateQueries({ queryKey: ['blocks'] });
}, },
}); });
@@ -131,7 +135,7 @@ export function AddImageBlock() {
}; };
useEffect(() => { useEffect(() => {
setValue("content", image); setValue('content', image);
}, [setValue, image]); }, [setValue, image]);
return ( return (
@@ -139,14 +143,14 @@ export function AddImageBlock() {
<button <button
type="button" type="button"
onClick={() => openModal()} onClick={() => openModal()}
className="inline-flex w-56 h-9 items-center justify-start gap-2.5 rounded-md px-2.5" className="inline-flex h-9 w-56 items-center justify-start gap-2.5 rounded-md px-2.5"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<CommandIcon width={12} height={12} className="text-zinc-500" /> <CommandIcon width={12} height={12} className="text-zinc-500" />
</div> </div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900"> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<span className="text-zinc-500 text-sm leading-none">I</span> <span className="text-sm leading-none text-zinc-500">I</span>
</div> </div>
</div> </div>
<div> <div>
@@ -191,48 +195,49 @@ export function AddImageBlock() {
onClick={closeModal} onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon <CancelIcon width={14} height={14} className="text-zinc-300" />
width={14}
height={14}
className="text-zinc-300"
/>
</button> </button>
</div> </div>
<Dialog.Description className="text-sm leading-tight text-zinc-400"> <Dialog.Description className="text-sm leading-tight text-zinc-400">
Pin your favorite image to Space then you can view every Pin your favorite image to Space then you can view every time that
time that you use Lume, your image will be broadcast to you use Lume, your image will be broadcast to Nostr Relay as well
Nostr Relay as well
</Dialog.Description> </Dialog.Description>
</div> </div>
</div> </div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3"> <div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0" className="mb-0 flex h-full w-full flex-col gap-4"
> >
<input <input
type={"hidden"} type={'hidden'}
{...register("content")} {...register('content')}
value={image} value={image}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400"> <label
htmlFor="title"
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
>
Title * Title *
</label> </label>
<div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] 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-[6px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20"> <div className="after:shadow-highlight relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] 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-[6px] after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
<input <input
type={"text"} type={'text'}
{...register("title", { {...register('title', {
required: true, required: true,
})} })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-md 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-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="shadow-input relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
</div> </div>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400"> <label
htmlFor="picture"
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
>
Picture Picture
</label> </label>
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950"> <div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
@@ -240,7 +245,7 @@ export function AddImageBlock() {
src={image} src={image}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt="content" alt="content"
className="relative z-10 max-h-[156px] h-auto w-[150px] object-cover rounded-md" className="relative z-10 h-auto max-h-[156px] w-[150px] rounded-md object-cover"
/> />
<div className="absolute bottom-3 right-3 z-10"> <div className="absolute bottom-3 right-3 z-10">
<button <button
@@ -257,7 +262,7 @@ export function AddImageBlock() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30" className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
> >
{loading ? ( {loading ? (
<svg <svg
@@ -282,7 +287,7 @@ export function AddImageBlock() {
/> />
</svg> </svg>
) : ( ) : (
"Confirm" 'Confirm'
)} )}
</button> </button>
</div> </div>

View File

@@ -1,14 +1,12 @@
import { getNotesByAuthors, removeBlock } from "@libs/storage"; import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Note } from "@shared/notes/note"; import { useVirtualizer } from '@tanstack/react-virtual';
import { NoteSkeleton } from "@shared/notes/skeleton"; import { useEffect, useRef } from 'react';
import { TitleBar } from "@shared/titleBar";
import { import { getNotesByAuthors, removeBlock } from '@libs/storage';
useInfiniteQuery,
useMutation, import { Note } from '@shared/notes/note';
useQueryClient, import { NoteSkeleton } from '@shared/notes/skeleton';
} from "@tanstack/react-query"; import { TitleBar } from '@shared/titleBar';
import { useVirtualizer } from "@tanstack/react-virtual";
import { useEffect, useRef } from "react";
const ITEM_PER_PAGE = 10; const ITEM_PER_PAGE = 10;
@@ -16,13 +14,9 @@ export function FeedBlock({ params }: { params: any }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage }: any = const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage }: any =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ["newsfeed", params.content], queryKey: ['newsfeed', params.content],
queryFn: async ({ pageParam = 0 }) => { queryFn: async ({ pageParam = 0 }) => {
return await getNotesByAuthors( return await getNotesByAuthors(params.content, ITEM_PER_PAGE, pageParam);
params.content,
ITEM_PER_PAGE,
pageParam,
);
}, },
getNextPageParam: (lastPage) => lastPage.nextCursor, getNextPageParam: (lastPage) => lastPage.nextCursor,
}); });
@@ -46,11 +40,7 @@ export function FeedBlock({ params }: { params: any }) {
return; return;
} }
if ( if (lastItem.index >= notes.length - 1 && hasNextPage && !isFetchingNextPage) {
lastItem.index >= notes.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage(); fetchNextPage();
} }
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]); }, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
@@ -60,7 +50,7 @@ export function FeedBlock({ params }: { params: any }) {
return removeBlock(id); return removeBlock(id);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["blocks"] }); queryClient.invalidateQueries({ queryKey: ['blocks'] });
}, },
}); });
@@ -76,14 +66,14 @@ export function FeedBlock({ params }: { params: any }) {
}; };
return ( return (
<div className="shrink-0 w-[400px] border-r border-zinc-900"> <div className="w-[400px] shrink-0 border-r border-zinc-900">
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} /> <TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
<div <div
ref={parentRef} ref={parentRef}
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto" className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5"
style={{ contain: "strict" }} style={{ contain: 'strict' }}
> >
{status === "loading" ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3"> <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<NoteSkeleton /> <NoteSkeleton />
@@ -100,8 +90,7 @@ export function FeedBlock({ params }: { params: any }) {
className="absolute left-0 top-0 w-full" className="absolute left-0 top-0 w-full"
style={{ style={{
transform: `translateY(${ transform: `translateY(${
itemsVirtualizer[0].start - itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
rowVirtualizer.options.scrollMargin
}px)`, }px)`,
}} }}
> >

View File

@@ -1,12 +1,16 @@
import { useNewsfeed } from "@app/space/hooks/useNewsfeed"; import { useInfiniteQuery } from '@tanstack/react-query';
import { getNotes } from "@libs/storage"; import { useVirtualizer } from '@tanstack/react-virtual';
import { Note } from "@shared/notes/note"; import { useEffect, useRef } from 'react';
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar"; import { useNewsfeed } from '@app/space/hooks/useNewsfeed';
import { useNote } from "@stores/note";
import { useInfiniteQuery } from "@tanstack/react-query"; import { getNotes } from '@libs/storage';
import { useVirtualizer } from "@tanstack/react-virtual";
import { useEffect, useRef } from "react"; import { Note } from '@shared/notes/note';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { useNote } from '@stores/note';
const ITEM_PER_PAGE = 10; const ITEM_PER_PAGE = 10;
@@ -18,15 +22,9 @@ export function FollowingBlock({ block }: { block: number }) {
state.toggleHasNewNote, state.toggleHasNewNote,
]); ]);
const { const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch }: any =
status, useInfiniteQuery({
data, queryKey: ['newsfeed-circle'],
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
}: any = useInfiniteQuery({
queryKey: ["newsfeed-circle"],
queryFn: async ({ pageParam = 0 }) => { queryFn: async ({ pageParam = 0 }) => {
return await getNotes(ITEM_PER_PAGE, pageParam); return await getNotes(ITEM_PER_PAGE, pageParam);
}, },
@@ -52,11 +50,7 @@ export function FollowingBlock({ block }: { block: number }) {
return; return;
} }
if ( if (lastItem.index >= notes.length - 1 && hasNextPage && !isFetchingNextPage) {
lastItem.index >= notes.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage(); fetchNextPage();
} }
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]); }, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
@@ -81,14 +75,14 @@ export function FollowingBlock({ block }: { block: number }) {
}; };
return ( return (
<div className="shrink-0 relative w-[400px] border-r border-zinc-900"> <div className="relative w-[400px] shrink-0 border-r border-zinc-900">
<TitleBar title="Your Circle" /> <TitleBar title="Your Circle" />
{hasNewNote && ( {hasNewNote && (
<div className="z-50 absolute top-12 left-1/2 transform -translate-x-1/2"> <div className="absolute left-1/2 top-12 z-50 -translate-x-1/2 transform">
<button <button
type="button" type="button"
onClick={() => refreshFirstPage()} onClick={() => refreshFirstPage()}
className="inline-flex items-center justify-center w-min px-3.5 py-1.5 rounded-full bg-fuchsia-500 hover:bg-fuchsia-600 border border-fuchsia-800/50 text-sm" className="inline-flex w-min items-center justify-center rounded-full border border-fuchsia-800/50 bg-fuchsia-500 px-3.5 py-1.5 text-sm hover:bg-fuchsia-600"
> >
Newest Newest
</button> </button>
@@ -96,10 +90,10 @@ export function FollowingBlock({ block }: { block: number }) {
)} )}
<div <div
ref={parentRef} ref={parentRef}
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto" className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5"
style={{ contain: "strict" }} style={{ contain: 'strict' }}
> >
{status === "loading" ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3"> <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<NoteSkeleton /> <NoteSkeleton />
@@ -116,8 +110,7 @@ export function FollowingBlock({ block }: { block: number }) {
className="absolute left-0 top-0 w-full" className="absolute left-0 top-0 w-full"
style={{ style={{
transform: `translateY(${ transform: `translateY(${
itemsVirtualizer[0].start - itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
rowVirtualizer.options.scrollMargin
}px)`, }px)`,
}} }}
> >

View File

@@ -1,8 +1,11 @@
import { removeBlock } from "@libs/storage"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { CancelIcon } from "@shared/icons";
import { Image } from "@shared/image"; import { removeBlock } from '@libs/storage';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { CancelIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
export function ImageBlock({ params }: { params: any }) { export function ImageBlock({ params }: { params: any }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -12,22 +15,20 @@ export function ImageBlock({ params }: { params: any }) {
return removeBlock(id); return removeBlock(id);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["blocks"] }); queryClient.invalidateQueries({ queryKey: ['blocks'] });
}, },
}); });
return ( return (
<div className="shrink-0 w-[350px] h-full flex flex-col justify-between border-r border-zinc-900"> <div className="flex h-full w-[350px] shrink-0 flex-col justify-between border-r border-zinc-900">
<div className="relative flex-1 w-full h-full p-3 overflow-hidden"> <div className="relative h-full w-full flex-1 overflow-hidden p-3">
<div className="absolute top-3 left-0 w-full h-16 px-3"> <div className="absolute left-0 top-3 h-16 w-full px-3">
<div className="h-16 rounded-t-xl overflow-hidden flex items-center justify-between px-5"> <div className="flex h-16 items-center justify-between overflow-hidden rounded-t-xl px-5">
<h3 className="text-white font-medium drop-shadow-lg"> <h3 className="font-medium text-white drop-shadow-lg">{params.title}</h3>
{params.title}
</h3>
<button <button
type="button" type="button"
onClick={() => block.mutate(params.id)} onClick={() => block.mutate(params.id)}
className="inline-flex h-7 w-7 rounded-md items-center justify-center bg-white/30 backdrop-blur-lg" className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-white/30 backdrop-blur-lg"
> >
<CancelIcon width={16} height={16} className="text-white" /> <CancelIcon width={16} height={16} className="text-white" />
</button> </button>
@@ -37,7 +38,7 @@ export function ImageBlock({ params }: { params: any }) {
src={params.content} src={params.content}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt={params.title} alt={params.title}
className="w-full h-full object-cover rounded-xl border-t border-zinc-800/50" className="h-full w-full rounded-xl border-t border-zinc-800/50 object-cover"
/> />
</div> </div>
</div> </div>

View File

@@ -1,16 +1,20 @@
import { useLiveThread } from "@app/space/hooks/useLiveThread"; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getNoteByID, removeBlock } from "@libs/storage";
import { Kind1 } from "@shared/notes/contents/kind1"; import { useLiveThread } from '@app/space/hooks/useLiveThread';
import { Kind1063 } from "@shared/notes/contents/kind1063";
import { NoteMetadata } from "@shared/notes/metadata"; import { getNoteByID, removeBlock } from '@libs/storage';
import { NoteReplyForm } from "@shared/notes/replies/form";
import { RepliesList } from "@shared/notes/replies/list"; import { Kind1 } from '@shared/notes/contents/kind1';
import { NoteSkeleton } from "@shared/notes/skeleton"; import { Kind1063 } from '@shared/notes/contents/kind1063';
import { TitleBar } from "@shared/titleBar"; import { NoteMetadata } from '@shared/notes/metadata';
import { User } from "@shared/user"; import { NoteReplyForm } from '@shared/notes/replies/form';
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { RepliesList } from '@shared/notes/replies/list';
import { useAccount } from "@utils/hooks/useAccount"; import { NoteSkeleton } from '@shared/notes/skeleton';
import { parser } from "@utils/parser"; import { TitleBar } from '@shared/titleBar';
import { User } from '@shared/user';
import { useAccount } from '@utils/hooks/useAccount';
import { parser } from '@utils/parser';
export function ThreadBlock({ params }: { params: any }) { export function ThreadBlock({ params }: { params: any }) {
useLiveThread(params.content); useLiveThread(params.content);
@@ -18,9 +22,9 @@ export function ThreadBlock({ params }: { params: any }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { account } = useAccount(); const { account } = useAccount();
const { status, data } = useQuery(["thread", params.content], async () => { const { status, data } = useQuery(['thread', params.content], async () => {
const res = await getNoteByID(params.content); const res = await getNoteByID(params.content);
res["content"] = parser(res); res['content'] = parser(res);
return res; return res;
}); });
@@ -29,17 +33,17 @@ export function ThreadBlock({ params }: { params: any }) {
return removeBlock(id); return removeBlock(id);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["blocks"] }); queryClient.invalidateQueries({ queryKey: ['blocks'] });
}, },
}); });
return ( return (
<div className="shrink-0 w-[400px] border-r border-zinc-900"> <div className="w-[400px] shrink-0 border-r border-zinc-900">
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} /> <TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
<div className="scrollbar-hide flex w-full h-full flex-col gap-1.5 pt-1.5 pb-20 overflow-y-auto"> <div className="scrollbar-hide flex h-full w-full flex-col gap-1.5 overflow-y-auto pb-20 pt-1.5">
{status === "loading" ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20"> <div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
@@ -56,12 +60,9 @@ export function ThreadBlock({ params }: { params: any }) {
/> />
</div> </div>
</div> </div>
<div className="mt-3 bg-zinc-900 rounded-md"> <div className="mt-3 rounded-md bg-zinc-900">
{account && ( {account && (
<NoteReplyForm <NoteReplyForm rootID={params.content} userPubkey={account.pubkey} />
rootID={params.content}
userPubkey={account.pubkey}
/>
)} )}
</div> </div>
</div> </div>

View File

@@ -1,8 +1,10 @@
import { createReplyNote } from "@libs/storage"; import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { RelayContext } from "@shared/relayProvider"; import { useContext, useEffect, useRef } from 'react';
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useContext, useEffect, useRef } from "react"; import { createReplyNote } from '@libs/storage';
import { RelayContext } from '@shared/relayProvider';
export function useLiveThread(id: string) { export function useLiveThread(id: string) {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -18,24 +20,24 @@ export function useLiveThread(id: string) {
data.kind, data.kind,
data.tags, data.tags,
data.content, data.content,
data.created_at, data.created_at
); );
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["replies", id] }); queryClient.invalidateQueries({ queryKey: ['replies', id] });
}, },
}); });
useEffect(() => { useEffect(() => {
const filter: NDKFilter = { const filter: NDKFilter = {
kinds: [1], kinds: [1],
"#e": [id], '#e': [id],
since: now.current, since: now.current,
}; };
const sub = ndk.subscribe(filter, { closeOnEose: false }); const sub = ndk.subscribe(filter, { closeOnEose: false });
sub.addListener("event", (event: NDKEvent) => { sub.addListener('event', (event: NDKEvent) => {
thread.mutate(event); thread.mutate(event);
}); });

View File

@@ -1,9 +1,13 @@
import { createNote } from "@libs/storage"; import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; import { useContext, useEffect, useRef } from 'react';
import { RelayContext } from "@shared/relayProvider";
import { useNote } from "@stores/note"; import { createNote } from '@libs/storage';
import { useAccount } from "@utils/hooks/useAccount";
import { useContext, useEffect, useRef } from "react"; import { RelayContext } from '@shared/relayProvider';
import { useNote } from '@stores/note';
import { useAccount } from '@utils/hooks/useAccount';
export function useNewsfeed() { export function useNewsfeed() {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
@@ -14,7 +18,7 @@ export function useNewsfeed() {
const { status, account } = useAccount(); const { status, account } = useAccount();
useEffect(() => { useEffect(() => {
if (status === "success" && account) { if (status === 'success' && account) {
const follows = account ? JSON.parse(account.follows) : []; const follows = account ? JSON.parse(account.follows) : [];
const filter: NDKFilter = { const filter: NDKFilter = {
@@ -25,8 +29,8 @@ export function useNewsfeed() {
sub.current = ndk.subscribe(filter, { closeOnEose: false }); sub.current = ndk.subscribe(filter, { closeOnEose: false });
sub.current.addListener("event", (event: NDKEvent) => { sub.current.addListener('event', (event: NDKEvent) => {
console.log("new note: ", event); console.log('new note: ', event);
// add to db // add to db
createNote( createNote(
event.id, event.id,
@@ -34,7 +38,7 @@ export function useNewsfeed() {
event.kind, event.kind,
event.tags, event.tags,
event.content, event.content,
event.created_at, event.created_at
); );
// notify user about created note // notify user about created note
toggleHasNewNote(true); toggleHasNewNote(true);

View File

@@ -1,11 +1,14 @@
import { AddBlock } from "@app/space/components/add"; import { useQuery } from '@tanstack/react-query';
import { FeedBlock } from "@app/space/components/blocks/feed";
import { FollowingBlock } from "@app/space/components/blocks/following"; import { AddBlock } from '@app/space/components/add';
import { ImageBlock } from "@app/space/components/blocks/image"; import { FeedBlock } from '@app/space/components/blocks/feed';
import { ThreadBlock } from "@app/space/components/blocks/thread"; import { FollowingBlock } from '@app/space/components/blocks/following';
import { getBlocks } from "@libs/storage"; import { ImageBlock } from '@app/space/components/blocks/image';
import { LoaderIcon } from "@shared/icons"; import { ThreadBlock } from '@app/space/components/blocks/thread';
import { useQuery } from "@tanstack/react-query";
import { getBlocks } from '@libs/storage';
import { LoaderIcon } from '@shared/icons';
export function SpaceScreen() { export function SpaceScreen() {
const { const {
@@ -13,7 +16,7 @@ export function SpaceScreen() {
data: blocks, data: blocks,
isFetching, isFetching,
} = useQuery( } = useQuery(
["blocks"], ['blocks'],
async () => { async () => {
return await getBlocks(); return await getBlocks();
}, },
@@ -22,20 +25,20 @@ export function SpaceScreen() {
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}, }
); );
return ( return (
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide"> <div className="scrollbar-hide flex h-full w-full flex-nowrap overflow-x-auto overflow-y-hidden">
<FollowingBlock block={1} /> <FollowingBlock block={1} />
{status === "loading" ? ( {status === 'loading' ? (
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900"> <div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="group overflow-hidden h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900" className="group flex h-11 w-full items-center justify-between overflow-hidden border-b border-zinc-900 px-3"
/> />
<div className="w-full flex-1 flex items-center justify-center p-3"> <div className="flex w-full flex-1 items-center justify-center p-3">
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
</div> </div>
</div> </div>
@@ -54,23 +57,23 @@ export function SpaceScreen() {
}) })
)} )}
{isFetching && ( {isFetching && (
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900"> <div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="group overflow-hidden h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900" className="group flex h-11 w-full items-center justify-between overflow-hidden border-b border-zinc-900 px-3"
/> />
<div className="w-full flex-1 flex items-center justify-center p-3"> <div className="flex w-full flex-1 items-center justify-center p-3">
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
</div> </div>
</div> </div>
)} )}
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900"> <div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
<div className="w-full h-full inline-flex items-center justify-center"> <div className="inline-flex h-full w-full items-center justify-center">
<AddBlock /> <AddBlock />
</div> </div>
</div> </div>
<div className="shrink-0 w-[350px]" /> <div className="w-[350px] shrink-0" />
</div> </div>
); );
} }

View File

@@ -1,22 +1,20 @@
import { FollowIcon, LoaderIcon, UnfollowIcon } from "@shared/icons"; import { useQuery } from '@tanstack/react-query';
import { Image } from "@shared/image"; import { useEffect, useState } from 'react';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useQuery } from "@tanstack/react-query"; import { FollowIcon, LoaderIcon, UnfollowIcon } from '@shared/icons';
import { useSocial } from "@utils/hooks/useSocial"; import { Image } from '@shared/image';
import { compactNumber } from "@utils/number";
import { shortenKey } from "@utils/shortenKey"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useEffect, useState } from "react";
import { useSocial } from '@utils/hooks/useSocial';
import { compactNumber } from '@utils/number';
import { shortenKey } from '@utils/shortenKey';
export function Profile({ data }: { data: any }) { export function Profile({ data }: { data: any }) {
const { status, data: userStats } = useQuery( const { status, data: userStats } = useQuery(['user-stats', data.pubkey], async () => {
["user-stats", data.pubkey], const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
async () => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${data.pubkey}`,
);
return res.json(); return res.json();
}, });
);
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null; const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
const profile = embedProfile; const profile = embedProfile;
@@ -45,7 +43,7 @@ export function Profile({ data }: { data: any }) {
}; };
useEffect(() => { useEffect(() => {
if (status === "success" && userFollows) { if (status === 'success' && userFollows) {
if (userFollows.includes(data.pubkey)) { if (userFollows.includes(data.pubkey)) {
setFollowed(true); setFollowed(true);
} }
@@ -55,7 +53,7 @@ export function Profile({ data }: { data: any }) {
if (!profile) if (!profile)
return ( return (
<div className="rounded-md bg-zinc-900 px-5 py-5"> <div className="rounded-md bg-zinc-900 px-5 py-5">
<p>Can't fetch profile</p> <p>Can&apos;t fetch profile</p>
</div> </div>
); );
@@ -63,27 +61,27 @@ export function Profile({ data }: { data: any }) {
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-5 py-5"> <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-5 py-5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<div className="w-11 h-11 shrink-0"> <div className="h-11 w-11 shrink-0">
<Image <Image
src={profile.picture} src={profile.picture}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
className="w-11 h-11 object-cover rounded-lg" className="h-11 w-11 rounded-lg object-cover"
/> />
</div> </div>
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<h3 className="max-w-[15rem] truncate font-semibold text-zinc-100 leading-none"> <h3 className="max-w-[15rem] truncate font-semibold leading-none text-zinc-100">
{profile.display_name || profile.name} {profile.display_name || profile.name}
</h3> </h3>
<p className="max-w-[10rem] truncate text-sm text-zinc-400 leading-none"> <p className="max-w-[10rem] truncate text-sm leading-none text-zinc-400">
{profile.nip05 || shortenKey(data.pubkey)} {profile.nip05 || shortenKey(data.pubkey)}
</p> </p>
</div> </div>
</div> </div>
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
{socialStatus === "loading" ? ( {socialStatus === 'loading' ? (
<button <button
type="button" type="button"
className="inline-flex w-8 h-8 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500" className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500"
> >
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
</button> </button>
@@ -91,17 +89,17 @@ export function Profile({ data }: { data: any }) {
<button <button
type="button" type="button"
onClick={() => unfollowUser(data.pubkey)} onClick={() => unfollowUser(data.pubkey)}
className="inline-flex w-8 h-8 items-center justify-center rounded-md text-zinc-400 bg-zinc-800 hover:bg-fuchsia-500 hover:text-white" className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-fuchsia-500 hover:text-white"
> >
<UnfollowIcon className="w-4 h-4" /> <UnfollowIcon className="h-4 w-4" />
</button> </button>
) : ( ) : (
<button <button
type="button" type="button"
onClick={() => followUser(data.pubkey)} onClick={() => followUser(data.pubkey)}
className="inline-flex w-8 h-8 items-center justify-center rounded-md text-zinc-400 bg-zinc-800 hover:bg-fuchsia-500 hover:text-white" className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-fuchsia-500 hover:text-white"
> >
<FollowIcon className="w-4 h-4" /> <FollowIcon className="h-4 w-4" />
</button> </button>
)} )}
</div> </div>
@@ -112,37 +110,31 @@ export function Profile({ data }: { data: any }) {
</p> </p>
</div> </div>
<div className="mt-8"> <div className="mt-8">
{status === "loading" ? ( {status === 'loading' ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
<div className="w-full flex items-center gap-8"> <div className="flex w-full items-center gap-8">
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100"> <span className="font-semibold leading-none text-zinc-100">
{userStats.stats[data.pubkey].followers_pubkey_count ?? 0} {userStats.stats[data.pubkey].followers_pubkey_count ?? 0}
</span> </span>
<span className="leading-none text-sm text-zinc-400"> <span className="text-sm leading-none text-zinc-400">Followers</span>
Followers
</span>
</div> </div>
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100"> <span className="font-semibold leading-none text-zinc-100">
{userStats.stats[data.pubkey].pub_following_pubkey_count ?? 0} {userStats.stats[data.pubkey].pub_following_pubkey_count ?? 0}
</span> </span>
<span className="leading-none text-sm text-zinc-400"> <span className="text-sm leading-none text-zinc-400">Following</span>
Following
</span>
</div> </div>
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100"> <span className="font-semibold leading-none text-zinc-100">
{userStats.stats[data.pubkey].zaps_received {userStats.stats[data.pubkey].zaps_received
? compactNumber.format( ? compactNumber.format(
userStats.stats[data.pubkey].zaps_received.msats / 1000, userStats.stats[data.pubkey].zaps_received.msats / 1000
) )
: 0} : 0}
</span> </span>
<span className="leading-none text-sm text-zinc-400"> <span className="text-sm leading-none text-zinc-400">Zaps received</span>
Zaps received
</span>
</div> </div>
</div> </div>
)} )}

View File

@@ -1,30 +1,31 @@
import { Note } from "@shared/notes/note"; import { useQuery } from '@tanstack/react-query';
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar"; import { Note } from '@shared/notes/note';
import { useQuery } from "@tanstack/react-query"; import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
export function TrendingNotes() { export function TrendingNotes() {
const { status, data, error } = useQuery(["trending-notes"], async () => { const { status, data, error } = useQuery(['trending-notes'], async () => {
const res = await fetch("https://api.nostr.band/v0/trending/notes"); const res = await fetch('https://api.nostr.band/v0/trending/notes');
if (!res.ok) { if (!res.ok) {
throw new Error("Error"); throw new Error('Error');
} }
return res.json(); return res.json();
}); });
return ( return (
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900"> <div className="flex w-[360px] shrink-0 flex-col border-r border-zinc-900">
<TitleBar title="Trending Posts" /> <TitleBar title="Trending Posts" />
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"> <div className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5">
{error && <p>Failed to fetch</p>} {error && <p>Failed to fetch</p>}
{status === "loading" ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20"> <div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : ( ) : (
<div className="relative w-full flex flex-col pt-1.5"> <div className="relative flex w-full flex-col pt-1.5">
{data.notes.map((item) => ( {data.notes.map((item) => (
<Note key={item.id} event={item.event} /> <Note key={item.id} event={item.event} />
))} ))}

View File

@@ -1,30 +1,32 @@
import { Profile } from "@app/trending/components/profile"; import { useQuery } from '@tanstack/react-query';
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar"; import { Profile } from '@app/trending/components/profile';
import { useQuery } from "@tanstack/react-query";
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
export function TrendingProfiles() { export function TrendingProfiles() {
const { status, data, error } = useQuery(["trending-profiles"], async () => { const { status, data, error } = useQuery(['trending-profiles'], async () => {
const res = await fetch("https://api.nostr.band/v0/trending/profiles"); const res = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) { if (!res.ok) {
throw new Error("Error"); throw new Error('Error');
} }
return res.json(); return res.json();
}); });
return ( return (
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900"> <div className="flex w-[360px] shrink-0 flex-col border-r border-zinc-900">
<TitleBar title="Trending Profiles" /> <TitleBar title="Trending Profiles" />
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"> <div className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5">
{error && <p>Failed to fetch</p>} {error && <p>Failed to fetch</p>}
{status === "loading" ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20"> <div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : ( ) : (
<div className="relative w-full flex flex-col gap-3 px-3 pt-3"> <div className="relative flex w-full flex-col gap-3 px-3 pt-3">
{data.profiles.map((item) => ( {data.profiles.map((item) => (
<Profile key={item.pubkey} data={item} /> <Profile key={item.pubkey} data={item} />
))} ))}

View File

@@ -1,9 +1,9 @@
import { TrendingNotes } from "@app/trending/components/trendingNotes"; import { TrendingNotes } from '@app/trending/components/trendingNotes';
import { TrendingProfiles } from "@app/trending/components/trendingProfiles"; import { TrendingProfiles } from '@app/trending/components/trendingProfiles';
export function TrendingScreen() { export function TrendingScreen() {
return ( return (
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide"> <div className="scrollbar-hide flex h-full w-full flex-nowrap overflow-x-auto overflow-y-hidden">
<TrendingProfiles /> <TrendingProfiles />
<TrendingNotes /> <TrendingNotes />
</div> </div>

View File

@@ -1,14 +1,16 @@
import { NDKFilter } from "@nostr-dev-kit/ndk"; import { NDKFilter } from '@nostr-dev-kit/ndk';
import { Note } from "@shared/notes/note"; import { useQuery } from '@tanstack/react-query';
import { RelayContext } from "@shared/relayProvider"; import { useContext } from 'react';
import { useQuery } from "@tanstack/react-query";
import { dateToUnix, getHourAgo } from "@utils/date"; import { Note } from '@shared/notes/note';
import { LumeEvent } from "@utils/types"; import { RelayContext } from '@shared/relayProvider';
import { useContext } from "react";
import { dateToUnix, getHourAgo } from '@utils/date';
import { LumeEvent } from '@utils/types';
export function UserFeed({ pubkey }: { pubkey: string }) { export function UserFeed({ pubkey }: { pubkey: string }) {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
const { status, data } = useQuery(["user-feed", pubkey], async () => { const { status, data } = useQuery(['user-feed', pubkey], async () => {
const now = new Date(); const now = new Date();
const filter: NDKFilter = { const filter: NDKFilter = {
kinds: [1], kinds: [1],
@@ -21,7 +23,7 @@ export function UserFeed({ pubkey }: { pubkey: string }) {
return ( return (
<div className="w-full max-w-[400px] px-2 pb-10"> <div className="w-full max-w-[400px] px-2 pb-10">
{status === "loading" ? ( {status === 'loading' ? (
<div className="px-3"> <div className="px-3">
<p>Loading...</p> <p>Loading...</p>
</div> </div>

View File

@@ -1,54 +1,49 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from '@tanstack/react-query';
import { compactNumber } from "@utils/number";
import { compactNumber } from '@utils/number';
export function UserMetadata({ pubkey }: { pubkey: string }) { export function UserMetadata({ pubkey }: { pubkey: string }) {
const { status, data } = useQuery(["user-metadata", pubkey], async () => { const { status, data } = useQuery(['user-metadata', pubkey], async () => {
const res = await fetch( const res = await fetch(`https://api.nostr.band/v0/stats/profile/${pubkey}`);
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
);
if (!res.ok) { if (!res.ok) {
throw new Error("Error"); throw new Error('Error');
} }
return await res.json(); return await res.json();
}); });
if (status === "loading") { if (status === 'loading') {
return <p>Loading...</p>; return <p>Loading...</p>;
} }
return ( return (
<div className="w-full flex items-center gap-10"> <div className="flex w-full items-center gap-10">
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100"> <span className="font-semibold leading-none text-zinc-100">
{data.stats[pubkey].followers_pubkey_count ?? 0} {data.stats[pubkey].followers_pubkey_count ?? 0}
</span> </span>
<span className="leading-none text-sm text-zinc-400">Followers</span> <span className="text-sm leading-none text-zinc-400">Followers</span>
</div> </div>
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100"> <span className="font-semibold leading-none text-zinc-100">
{data.stats[pubkey].pub_following_pubkey_count ?? 0} {data.stats[pubkey].pub_following_pubkey_count ?? 0}
</span> </span>
<span className="leading-none text-sm text-zinc-400">Following</span> <span className="text-sm leading-none text-zinc-400">Following</span>
</div> </div>
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100"> <span className="font-semibold leading-none text-zinc-100">
{data.stats[pubkey].zaps_received {data.stats[pubkey].zaps_received
? compactNumber.format( ? compactNumber.format(data.stats[pubkey].zaps_received.msats / 1000)
data.stats[pubkey].zaps_received.msats / 1000,
)
: 0} : 0}
</span> </span>
<span className="leading-none text-sm text-zinc-400"> <span className="text-sm leading-none text-zinc-400">Zaps received</span>
Zaps received
</span>
</div> </div>
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100"> <span className="font-semibold leading-none text-zinc-100">
{data.stats[pubkey].zaps_sent {data.stats[pubkey].zaps_sent
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000) ? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
: 0} : 0}
</span> </span>
<span className="leading-none text-sm text-zinc-400">Zaps sent</span> <span className="text-sm leading-none text-zinc-400">Zaps sent</span>
</div> </div>
</div> </div>
); );

View File

@@ -1,16 +1,20 @@
import { UserFeed } from "@app/user/components/feed"; import { Tab } from '@headlessui/react';
import { UserMetadata } from "@app/user/components/metadata"; import { Fragment, useEffect, useState } from 'react';
import { Tab } from "@headlessui/react"; import { Link, useParams } from 'react-router-dom';
import { EditProfileModal } from "@shared/editProfileModal";
import { ThreadsIcon, ZapIcon } from "@shared/icons"; import { UserFeed } from '@app/user/components/feed';
import { Image } from "@shared/image"; import { UserMetadata } from '@app/user/components/metadata';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useAccount } from "@utils/hooks/useAccount"; import { EditProfileModal } from '@shared/editProfileModal';
import { useProfile } from "@utils/hooks/useProfile"; import { ThreadsIcon, ZapIcon } from '@shared/icons';
import { useSocial } from "@utils/hooks/useSocial"; import { Image } from '@shared/image';
import { shortenKey } from "@utils/shortenKey";
import { Fragment, useEffect, useState } from "react"; import { DEFAULT_AVATAR } from '@stores/constants';
import { Link, useParams } from "react-router-dom";
import { useAccount } from '@utils/hooks/useAccount';
import { useProfile } from '@utils/hooks/useProfile';
import { useSocial } from '@utils/hooks/useSocial';
import { shortenKey } from '@utils/shortenKey';
export function UserScreen() { export function UserScreen() {
const { pubkey } = useParams(); const { pubkey } = useParams();
@@ -43,7 +47,7 @@ export function UserScreen() {
}; };
useEffect(() => { useEffect(() => {
if (status === "success" && userFollows) { if (status === 'success' && userFollows) {
if (userFollows.includes(pubkey)) { if (userFollows.includes(pubkey)) {
setFollowed(true); setFollowed(true);
} }
@@ -54,39 +58,39 @@ export function UserScreen() {
<div className="h-full w-full overflow-y-auto"> <div className="h-full w-full overflow-y-auto">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="h-11 w-full flex items-center px-3 border-b border-zinc-900" className="flex h-11 w-full items-center border-b border-zinc-900 px-3"
/> />
<div className="w-full h-56 bg-zinc-100"> <div className="h-56 w-full bg-zinc-100">
<Image <Image
src={user?.banner} src={user?.banner}
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg" fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
alt={"banner"} alt={'banner'}
className="w-full h-full object-cover" className="h-full w-full object-cover"
/> />
</div> </div>
<div className="w-full -mt-7"> <div className="-mt-7 w-full">
<div className="px-5"> <div className="px-5">
<Image <Image
src={user?.image} src={user?.image}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="w-14 h-14 rounded-md ring-2 ring-black" className="h-14 w-14 rounded-md ring-2 ring-black"
/> />
<div className="flex-1 flex flex-col gap-4 mt-2"> <div className="mt-2 flex flex-1 flex-col gap-4">
<div className="flex items-center gap-16"> <div className="flex items-center gap-16">
<div className="inline-flex flex-col gap-1.5"> <div className="inline-flex flex-col gap-1.5">
<h5 className="font-semibold text-lg leading-none"> <h5 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name || "No name"} {user?.displayName || user?.name || 'No name'}
</h5> </h5>
<span className="max-w-[15rem] text-sm truncate leading-none text-zinc-500"> <span className="max-w-[15rem] truncate text-sm leading-none text-zinc-500">
{user?.nip05 || shortenKey(pubkey)} {user?.nip05 || shortenKey(pubkey)}
</span> </span>
</div> </div>
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
{status === "loading" ? ( {status === 'loading' ? (
<button <button
type="button" type="button"
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium" className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
> >
Loading... Loading...
</button> </button>
@@ -94,7 +98,7 @@ export function UserScreen() {
<button <button
type="button" type="button"
onClick={() => unfollowUser(pubkey)} onClick={() => unfollowUser(pubkey)}
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium" className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
> >
Unfollow Unfollow
</button> </button>
@@ -102,29 +106,29 @@ export function UserScreen() {
<button <button
type="button" type="button"
onClick={() => followUser(pubkey)} onClick={() => followUser(pubkey)}
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium" className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
> >
Follow Follow
</button> </button>
)} )}
<Link <Link
to={`/app/chat/${pubkey}`} to={`/app/chat/${pubkey}`}
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium" className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
> >
Message Message
</Link> </Link>
<button <button
type="button" type="button"
className="inline-flex w-10 h-10 items-center justify-center rounded-md bg-zinc-900 group hover:bg-orange-500 text-sm font-medium" className="group inline-flex h-10 w-10 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-orange-500"
> >
<ZapIcon className="w-5 h-5" /> <ZapIcon className="h-5 w-5" />
</button> </button>
<span className="inline-flex mx-2 w-px h-4 bg-zinc-900" /> <span className="mx-2 inline-flex h-4 w-px bg-zinc-900" />
{account && account.pubkey === pubkey && <EditProfileModal />} {account && account.pubkey === pubkey && <EditProfileModal />}
</div> </div>
</div> </div>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<p className="mt-2 max-w-[500px] break-words select-text text-zinc-100"> <p className="mt-2 max-w-[500px] select-text break-words text-zinc-100">
{user?.about} {user?.about}
</p> </p>
<UserMetadata pubkey={pubkey} /> <UserMetadata pubkey={pubkey} />
@@ -133,18 +137,18 @@ export function UserScreen() {
</div> </div>
<div className="mt-8 w-full border-t border-zinc-900"> <div className="mt-8 w-full border-t border-zinc-900">
<Tab.Group> <Tab.Group>
<Tab.List className="px-5 mb-2"> <Tab.List className="mb-2 px-5">
<Tab as={Fragment}> <Tab as={Fragment}>
{({ selected }) => ( {({ selected }) => (
<button <button
type="button" type="button"
className={`${ className={`${
selected selected
? "text-fuchsia-500 border-fuchsia-500" ? 'border-fuchsia-500 text-fuchsia-500'
: "text-zinc-200 border-transparent" : 'border-transparent text-zinc-200'
} font-medium inline-flex items-center gap-2 h-10 border-t`} } inline-flex h-10 items-center gap-2 border-t font-medium`}
> >
<ThreadsIcon className="w-4 h-4" /> <ThreadsIcon className="h-4 w-4" />
Activities from 48 hours ago Activities from 48 hours ago
</button> </button>
)} )}

View File

@@ -15,7 +15,7 @@ button {
} }
.markdown { .markdown {
@apply prose prose-zinc max-w-none select-text break-words dark:prose-invert prose-p:mt-0 prose-p:mb-2 prose-p:last:mb-0 prose-p:leading-tight prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 hover:prose-a:text-fuchsia-600 prose-ol:mb-1 prose-ul:mb-1 prose-li:leading-tight prose-blockquote:m-0 prose-ol:m-0 prose-hr:mx-0 prose-hr:my-2; @apply prose prose-zinc max-w-none select-text break-words dark:prose-invert prose-p:mb-2 prose-p:mt-0 prose-p:leading-tight prose-p:last:mb-0 prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 hover:prose-a:text-fuchsia-600 prose-blockquote:m-0 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-li:leading-tight prose-hr:mx-0 prose-hr:my-2;
} }
/* For Webkit-based browsers (Chrome, Safari and Opera) */ /* For Webkit-based browsers (Chrome, Safari and Opera) */

View File

@@ -4,11 +4,14 @@ import NDK, {
NDKFilter, NDKFilter,
NDKKind, NDKKind,
NDKPrivateKeySigner, NDKPrivateKeySigner,
} from "@nostr-dev-kit/ndk"; } from '@nostr-dev-kit/ndk';
import { RelayContext } from "@shared/relayProvider"; import { useContext } from 'react';
import { FULL_RELAYS } from "@stores/constants";
import { useAccount } from "@utils/hooks/useAccount"; import { RelayContext } from '@shared/relayProvider';
import { useContext } from "react";
import { FULL_RELAYS } from '@stores/constants';
import { useAccount } from '@utils/hooks/useAccount';
export async function initNDK(relays?: string[]): Promise<NDK> { export async function initNDK(relays?: string[]): Promise<NDK> {
const opts: NDKConstructorParams = {}; const opts: NDKConstructorParams = {};
@@ -24,7 +27,7 @@ export async function initNDK(relays?: string[]): Promise<NDK> {
export async function prefetchEvents( export async function prefetchEvents(
ndk: NDK, ndk: NDK,
filter: NDKFilter, filter: NDKFilter
): Promise<Set<NDKEvent>> { ): Promise<Set<NDKEvent>> {
return new Promise((resolve) => { return new Promise((resolve) => {
const events: Map<string, NDKEvent> = new Map(); const events: Map<string, NDKEvent> = new Map();
@@ -33,12 +36,12 @@ export async function prefetchEvents(
closeOnEose: true, closeOnEose: true,
}); });
relaySetSubscription.on("event", (event: NDKEvent) => { relaySetSubscription.on('event', (event: NDKEvent) => {
event.ndk = ndk; event.ndk = ndk;
events.set(event.tagId(), event); events.set(event.tagId(), event);
}); });
relaySetSubscription.on("eose", () => { relaySetSubscription.on('eose', () => {
setTimeout(() => resolve(new Set(events.values())), 3000); setTimeout(() => resolve(new Set(events.values())), 3000);
}); });
}); });

View File

@@ -1,6 +1,7 @@
import { OPENGRAPH } from "@stores/constants"; import { FetchOptions, ResponseType, fetch } from '@tauri-apps/api/http';
import { FetchOptions, ResponseType, fetch } from "@tauri-apps/api/http"; import * as cheerio from 'cheerio';
import * as cheerio from "cheerio";
import { OPENGRAPH } from '@stores/constants';
interface ILinkPreviewOptions { interface ILinkPreviewOptions {
headers?: Record<string, string>; headers?: Record<string, string>;
@@ -27,64 +28,63 @@ function metaTag(doc: cheerio.CheerioAPI, type: string, attr: string) {
} }
function metaTagContent(doc: cheerio.CheerioAPI, type: string, attr: string) { function metaTagContent(doc: cheerio.CheerioAPI, type: string, attr: string) {
return doc(`meta[${attr}='${type}']`).attr("content"); return doc(`meta[${attr}='${type}']`).attr('content');
} }
function getTitle(doc: cheerio.CheerioAPI) { function getTitle(doc: cheerio.CheerioAPI) {
let title = let title =
metaTagContent(doc, "og:title", "property") || metaTagContent(doc, 'og:title', 'property') ||
metaTagContent(doc, "og:title", "name"); metaTagContent(doc, 'og:title', 'name');
if (!title) { if (!title) {
title = doc("title").text(); title = doc('title').text();
} }
return title; return title;
} }
function getSiteName(doc: cheerio.CheerioAPI) { function getSiteName(doc: cheerio.CheerioAPI) {
const siteName = const siteName =
metaTagContent(doc, "og:site_name", "property") || metaTagContent(doc, 'og:site_name', 'property') ||
metaTagContent(doc, "og:site_name", "name"); metaTagContent(doc, 'og:site_name', 'name');
return siteName; return siteName;
} }
function getDescription(doc: cheerio.CheerioAPI) { function getDescription(doc: cheerio.CheerioAPI) {
const description = const description =
metaTagContent(doc, "description", "name") || metaTagContent(doc, 'description', 'name') ||
metaTagContent(doc, "Description", "name") || metaTagContent(doc, 'Description', 'name') ||
metaTagContent(doc, "og:description", "property"); metaTagContent(doc, 'og:description', 'property');
return description; return description;
} }
function getMediaType(doc: cheerio.CheerioAPI) { function getMediaType(doc: cheerio.CheerioAPI) {
const node = metaTag(doc, "medium", "name"); const node = metaTag(doc, 'medium', 'name');
if (node) { if (node) {
const content = node.attr("content"); const content = node.attr('content');
return content === "image" ? "photo" : content; return content === 'image' ? 'photo' : content;
} }
return ( return (
metaTagContent(doc, "og:type", "property") || metaTagContent(doc, 'og:type', 'property') || metaTagContent(doc, 'og:type', 'name')
metaTagContent(doc, "og:type", "name")
); );
} }
function getImages( function getImages(
doc: cheerio.CheerioAPI, doc: cheerio.CheerioAPI,
rootUrl: string, rootUrl: string,
imagesPropertyType?: string, imagesPropertyType?: string
) { ) {
let images: string[] = []; let images: string[] = [];
let nodes: cheerio.Cheerio<cheerio.Element> | null; let nodes: cheerio.Cheerio<cheerio.Element> | null;
let src: string | undefined; let src: string | undefined;
let dic: Record<string, boolean> = {}; let dic: Record<string, boolean> = {};
const imagePropertyType = imagesPropertyType ?? "og"; const imagePropertyType = imagesPropertyType ?? 'og';
nodes = nodes =
metaTag(doc, `${imagePropertyType}:image`, "property") || metaTag(doc, `${imagePropertyType}:image`, 'property') ||
metaTag(doc, `${imagePropertyType}:image`, "name"); metaTag(doc, `${imagePropertyType}:image`, 'name');
if (nodes) { if (nodes) {
nodes.each((_: number, node: cheerio.Element) => { nodes.each((_: number, node: cheerio.Element) => {
if (node.type === "tag") { if (node.type === 'tag') {
src = node.attribs.content; src = node.attribs.content;
if (src) { if (src) {
src = new URL(src, rootUrl).href; src = new URL(src, rootUrl).href;
@@ -95,18 +95,18 @@ function getImages(
} }
if (images.length <= 0 && !imagesPropertyType) { if (images.length <= 0 && !imagesPropertyType) {
src = doc("link[rel=image_src]").attr("href"); src = doc('link[rel=image_src]').attr('href');
if (src) { if (src) {
src = new URL(src, rootUrl).href; src = new URL(src, rootUrl).href;
images = [src]; images = [src];
} else { } else {
nodes = doc("img"); nodes = doc('img');
if (nodes?.length) { if (nodes?.length) {
dic = {}; dic = {};
images = []; images = [];
nodes.each((_: number, node: cheerio.Element) => { nodes.each((_: number, node: cheerio.Element) => {
if (node.type === "tag") src = node.attribs.src; if (node.type === 'tag') src = node.attribs.src;
if (src && !dic[src]) { if (src && !dic[src]) {
dic[src] = true; dic[src] = true;
// width = node.attribs.width; // width = node.attribs.width;
@@ -135,34 +135,32 @@ function getVideos(doc: cheerio.CheerioAPI) {
let videoObj; let videoObj;
let index; let index;
const nodes = const nodes = metaTag(doc, 'og:video', 'property') || metaTag(doc, 'og:video', 'name');
metaTag(doc, "og:video", "property") || metaTag(doc, "og:video", "name");
if (nodes?.length) { if (nodes?.length) {
nodeTypes = nodeTypes =
metaTag(doc, "og:video:type", "property") || metaTag(doc, 'og:video:type', 'property') || metaTag(doc, 'og:video:type', 'name');
metaTag(doc, "og:video:type", "name");
nodeSecureUrls = nodeSecureUrls =
metaTag(doc, "og:video:secure_url", "property") || metaTag(doc, 'og:video:secure_url', 'property') ||
metaTag(doc, "og:video:secure_url", "name"); metaTag(doc, 'og:video:secure_url', 'name');
width = width =
metaTagContent(doc, "og:video:width", "property") || metaTagContent(doc, 'og:video:width', 'property') ||
metaTagContent(doc, "og:video:width", "name"); metaTagContent(doc, 'og:video:width', 'name');
height = height =
metaTagContent(doc, "og:video:height", "property") || metaTagContent(doc, 'og:video:height', 'property') ||
metaTagContent(doc, "og:video:height", "name"); metaTagContent(doc, 'og:video:height', 'name');
for (index = 0; index < nodes.length; index += 1) { for (index = 0; index < nodes.length; index += 1) {
const node = nodes[index]; const node = nodes[index];
if (node.type === "tag") video = node.attribs.content; if (node.type === 'tag') video = node.attribs.content;
nodeType = nodeTypes?.[index]; nodeType = nodeTypes?.[index];
if (nodeType?.type === "tag") { if (nodeType?.type === 'tag') {
videoType = nodeType ? nodeType.attribs.content : null; videoType = nodeType ? nodeType.attribs.content : null;
} }
nodeSecureUrl = nodeSecureUrls?.[index]; nodeSecureUrl = nodeSecureUrls?.[index];
if (nodeSecureUrl?.type === "tag") { if (nodeSecureUrl?.type === 'tag') {
videoSecureUrl = nodeSecureUrl ? nodeSecureUrl.attribs.content : null; videoSecureUrl = nodeSecureUrl ? nodeSecureUrl.attribs.content : null;
} }
@@ -173,7 +171,7 @@ function getVideos(doc: cheerio.CheerioAPI) {
width, width,
height, height,
}; };
if (videoType && videoType.indexOf("video/") === 0) { if (videoType && videoType.indexOf('video/') === 0) {
videos.splice(0, 0, videoObj); videos.splice(0, 0, videoObj);
} else { } else {
videos.push(videoObj); videos.push(videoObj);
@@ -195,11 +193,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
let nodes: cheerio.Cheerio<cheerio.Element> | never[] = []; let nodes: cheerio.Cheerio<cheerio.Element> | never[] = [];
let src: string | undefined; let src: string | undefined;
const relSelectors = [ const relSelectors = ['rel=icon', `rel="shortcut icon"`, 'rel=apple-touch-icon'];
"rel=icon",
`rel="shortcut icon"`,
"rel=apple-touch-icon",
];
relSelectors.forEach((relSelector) => { relSelectors.forEach((relSelector) => {
// look for all icon tags // look for all icon tags
@@ -208,7 +202,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
// collect all images from icon tags // collect all images from icon tags
if (nodes.length) { if (nodes.length) {
nodes.each((_: number, node: cheerio.Element) => { nodes.each((_: number, node: cheerio.Element) => {
if (node.type === "tag") src = node.attribs.href; if (node.type === 'tag') src = node.attribs.href;
if (src) { if (src) {
src = new URL(rootUrl).href; src = new URL(rootUrl).href;
images.push(src); images.push(src);
@@ -228,7 +222,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
function parseImageResponse(url: string, contentType: string) { function parseImageResponse(url: string, contentType: string) {
return { return {
url, url,
mediaType: "image", mediaType: 'image',
contentType, contentType,
favicons: [getDefaultFavicon(url)], favicons: [getDefaultFavicon(url)],
}; };
@@ -237,7 +231,7 @@ function parseImageResponse(url: string, contentType: string) {
function parseAudioResponse(url: string, contentType: string) { function parseAudioResponse(url: string, contentType: string) {
return { return {
url, url,
mediaType: "audio", mediaType: 'audio',
contentType, contentType,
favicons: [getDefaultFavicon(url)], favicons: [getDefaultFavicon(url)],
}; };
@@ -246,7 +240,7 @@ function parseAudioResponse(url: string, contentType: string) {
function parseVideoResponse(url: string, contentType: string) { function parseVideoResponse(url: string, contentType: string) {
return { return {
url, url,
mediaType: "video", mediaType: 'video',
contentType, contentType,
favicons: [getDefaultFavicon(url)], favicons: [getDefaultFavicon(url)],
}; };
@@ -255,7 +249,7 @@ function parseVideoResponse(url: string, contentType: string) {
function parseApplicationResponse(url: string, contentType: string) { function parseApplicationResponse(url: string, contentType: string) {
return { return {
url, url,
mediaType: "application", mediaType: 'application',
contentType, contentType,
favicons: [getDefaultFavicon(url)], favicons: [getDefaultFavicon(url)],
}; };
@@ -265,7 +259,7 @@ function parseTextResponse(
body: string, body: string,
url: string, url: string,
options: ILinkPreviewOptions = {}, options: ILinkPreviewOptions = {},
contentType?: string, contentType?: string
) { ) {
const doc = cheerio.load(body); const doc = cheerio.load(body);
@@ -274,7 +268,7 @@ function parseTextResponse(
title: getTitle(doc), title: getTitle(doc),
siteName: getSiteName(doc), siteName: getSiteName(doc),
description: getDescription(doc), description: getDescription(doc),
mediaType: getMediaType(doc) || "website", mediaType: getMediaType(doc) || 'website',
contentType, contentType,
images: getImages(doc, url, options.imagesPropertyType), images: getImages(doc, url, options.imagesPropertyType),
videos: getVideos(doc), videos: getVideos(doc),
@@ -286,21 +280,18 @@ function parseUnknownResponse(
body: string, body: string,
url: string, url: string,
options: ILinkPreviewOptions = {}, options: ILinkPreviewOptions = {},
contentType?: string, contentType?: string
) { ) {
return parseTextResponse(body, url, options, contentType); return parseTextResponse(body, url, options, contentType);
} }
function parseResponse( function parseResponse(response: IPreFetchedResource, options?: ILinkPreviewOptions) {
response: IPreFetchedResource,
options?: ILinkPreviewOptions,
) {
try { try {
let contentType = response.headers["content-type"]; let contentType = response.headers['content-type'];
// console.warn(`original content type`, contentType); // console.warn(`original content type`, contentType);
if (contentType?.indexOf(";")) { if (contentType?.indexOf(';')) {
// eslint-disable-next-line prefer-destructuring // eslint-disable-next-line prefer-destructuring
contentType = contentType.split(";")[0]; contentType = contentType.split(';')[0];
// console.warn(`splitting content type`, contentType); // console.warn(`splitting content type`, contentType);
} }
@@ -334,9 +325,7 @@ function parseResponse(
return parseUnknownResponse(htmlString, response.url, options); return parseUnknownResponse(htmlString, response.url, options);
} catch (e) { } catch (e) {
throw new Error( throw new Error(
`link-preview-js could not fetch link information ${( `link-preview-js could not fetch link information ${(e as any).toString()}`
e as any
).toString()}`,
); );
} }
} }
@@ -344,7 +333,7 @@ function parseResponse(
export async function getLinkPreview(text: string) { export async function getLinkPreview(text: string) {
const fetchUrl = text; const fetchUrl = text;
const options: FetchOptions = { const options: FetchOptions = {
method: "GET", method: 'GET',
timeout: 5, timeout: 5,
responseType: ResponseType.Text, responseType: ResponseType.Text,
}; };
@@ -352,7 +341,7 @@ export async function getLinkPreview(text: string) {
let response = await fetch(fetchUrl, options); let response = await fetch(fetchUrl, options);
if (response.status > 300 && response.status < 309) { if (response.status > 300 && response.status < 309) {
const forwardedUrl = response.headers.location || ""; const forwardedUrl = response.headers.location || '';
response = await fetch(forwardedUrl, options); response = await fetch(forwardedUrl, options);
} }

View File

@@ -1,5 +1,6 @@
import { getParentID } from "@utils/transform"; import Database from 'tauri-plugin-sql-api';
import Database from "tauri-plugin-sql-api";
import { getParentID } from '@utils/transform';
let db: null | Database = null; let db: null | Database = null;
@@ -9,16 +10,14 @@ export async function connect(): Promise<Database> {
if (db) { if (db) {
return db; return db;
} }
db = await Database.load("sqlite:lume.db"); db = await Database.load('sqlite:lume.db');
return db; return db;
} }
// get active account // get active account
export async function getActiveAccount() { export async function getActiveAccount() {
const db = await connect(); const db = await connect();
const result: any = await db.select( const result: any = await db.select('SELECT * FROM accounts WHERE is_active = 1;');
"SELECT * FROM accounts WHERE is_active = 1;",
);
if (result.length > 0) { if (result.length > 0) {
return result[0]; return result[0];
} else { } else {
@@ -30,7 +29,7 @@ export async function getActiveAccount() {
export async function getAccounts() { export async function getAccounts() {
const db = await connect(); const db = await connect();
return await db.select( return await db.select(
"SELECT * FROM accounts WHERE is_active = 0 ORDER BY created_at DESC;", 'SELECT * FROM accounts WHERE is_active = 0 ORDER BY created_at DESC;'
); );
} }
@@ -40,18 +39,18 @@ export async function createAccount(
pubkey: string, pubkey: string,
privkey: string, privkey: string,
follows?: string[][], follows?: string[][],
is_active?: number, is_active?: number
) { ) {
const db = await connect(); const db = await connect();
const res = await db.execute( const res = await db.execute(
"INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);", 'INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);',
[npub, pubkey, privkey, follows || "", is_active || 0], [npub, pubkey, privkey, follows || '', is_active || 0]
); );
if (res) { if (res) {
await createBlock( await createBlock(
0, 0,
"Preserve your freedom", 'Preserve your freedom',
"https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv", 'https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv'
); );
} }
const getAccount = await getActiveAccount(); const getAccount = await getActiveAccount();
@@ -62,13 +61,13 @@ export async function createAccount(
export async function updateAccount( export async function updateAccount(
column: string, column: string,
value: string | string[], value: string | string[],
pubkey: string, pubkey: string
) { ) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(`UPDATE accounts SET ${column} = ? WHERE pubkey = ?;`, [
`UPDATE accounts SET ${column} = ? WHERE pubkey = ?;`, value,
[value, pubkey], pubkey,
); ]);
} }
// count total notes // count total notes
@@ -82,7 +81,7 @@ export async function countTotalChannels() {
export async function countTotalNotes() { export async function countTotalNotes() {
const db = await connect(); const db = await connect();
const result = await db.select( const result = await db.select(
'SELECT COUNT(*) AS "total" FROM notes WHERE kind IN (1, 6);', 'SELECT COUNT(*) AS "total" FROM notes WHERE kind IN (1, 6);'
); );
return parseInt(result[0].total); return parseInt(result[0].total);
} }
@@ -95,12 +94,11 @@ export async function getNotes(limit: number, offset: number) {
const notes: any = { data: null, nextCursor: 0 }; const notes: any = { data: null, nextCursor: 0 };
const query: any = await db.select( const query: any = await db.select(
`SELECT * FROM notes WHERE kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`, `SELECT * FROM notes WHERE kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`
); );
notes["data"] = query; notes['data'] = query;
notes["nextCursor"] = notes['nextCursor'] = Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
return notes; return notes;
} }
@@ -109,18 +107,14 @@ export async function getNotes(limit: number, offset: number) {
export async function getNotesByPubkey(pubkey: string) { export async function getNotesByPubkey(pubkey: string) {
const db = await connect(); const db = await connect();
const res: any = await db.select( const res: any = await db.select(
`SELECT * FROM notes WHERE pubkey == "${pubkey}" AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC;`, `SELECT * FROM notes WHERE pubkey == "${pubkey}" AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC;`
); );
return res; return res;
} }
// get all notes by authors // get all notes by authors
export async function getNotesByAuthors( export async function getNotesByAuthors(authors: string, limit: number, offset: number) {
authors: string,
limit: number,
offset: number,
) {
const db = await connect(); const db = await connect();
const totalNotes = await countTotalNotes(); const totalNotes = await countTotalNotes();
const nextCursor = offset + limit; const nextCursor = offset + limit;
@@ -129,12 +123,11 @@ export async function getNotesByAuthors(
const notes: any = { data: null, nextCursor: 0 }; const notes: any = { data: null, nextCursor: 0 };
const query: any = await db.select( const query: any = await db.select(
`SELECT * FROM notes WHERE pubkey IN (${finalArray}) AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`, `SELECT * FROM notes WHERE pubkey IN (${finalArray}) AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`
); );
notes["data"] = query; notes['data'] = query;
notes["nextCursor"] = notes['nextCursor'] = Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
return notes; return notes;
} }
@@ -142,9 +135,7 @@ export async function getNotesByAuthors(
// get note by id // get note by id
export async function getNoteByID(event_id: string) { export async function getNoteByID(event_id: string) {
const db = await connect(); const db = await connect();
const result = await db.select( const result = await db.select(`SELECT * FROM notes WHERE event_id = "${event_id}";`);
`SELECT * FROM notes WHERE event_id = "${event_id}";`,
);
return result[0]; return result[0];
} }
@@ -155,15 +146,15 @@ export async function createNote(
kind: number, kind: number,
tags: any, tags: any,
content: string, content: string,
created_at: number, created_at: number
) { ) {
const db = await connect(); const db = await connect();
const account = await getActiveAccount(); const account = await getActiveAccount();
const parentID = getParentID(tags, event_id); const parentID = getParentID(tags, event_id);
return await db.execute( return await db.execute(
"INSERT OR IGNORE INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);", 'INSERT OR IGNORE INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);',
[event_id, account.id, pubkey, kind, tags, content, created_at, parentID], [event_id, account.id, pubkey, kind, tags, content, created_at, parentID]
); );
} }
@@ -171,7 +162,7 @@ export async function createNote(
export async function getReplies(parent_id: string) { export async function getReplies(parent_id: string) {
const db = await connect(); const db = await connect();
const result: any = await db.select( const result: any = await db.select(
`SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`, `SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`
); );
return result; return result;
} }
@@ -184,30 +175,26 @@ export async function createReplyNote(
kind: number, kind: number,
tags: any, tags: any,
content: string, content: string,
created_at: number, created_at: number
) { ) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(
"INSERT OR IGNORE INTO replies (event_id, parent_id, pubkey, kind, tags, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);", 'INSERT OR IGNORE INTO replies (event_id, parent_id, pubkey, kind, tags, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);',
[event_id, parent_id, pubkey, kind, tags, content, created_at], [event_id, parent_id, pubkey, kind, tags, content, created_at]
); );
} }
// get all channels // get all channels
export async function getChannels() { export async function getChannels() {
const db = await connect(); const db = await connect();
const result: any = await db.select( const result: any = await db.select('SELECT * FROM channels ORDER BY created_at DESC;');
"SELECT * FROM channels ORDER BY created_at DESC;",
);
return result; return result;
} }
// get channel by id // get channel by id
export async function getChannel(id: string) { export async function getChannel(id: string) {
const db = await connect(); const db = await connect();
const result = await db.select( const result = await db.select(`SELECT * FROM channels WHERE event_id = "${id}";`);
`SELECT * FROM channels WHERE event_id = "${id}";`,
);
return result[0]; return result[0];
} }
@@ -218,12 +205,12 @@ export async function createChannel(
name: string, name: string,
picture: string, picture: string,
about: string, about: string,
created_at: number, created_at: number
) { ) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(
"INSERT OR IGNORE INTO channels (event_id, pubkey, name, picture, about, created_at) VALUES (?, ?, ?, ?, ?, ?);", 'INSERT OR IGNORE INTO channels (event_id, pubkey, name, picture, about, created_at) VALUES (?, ?, ?, ?, ?, ?);',
[event_id, pubkey, name, picture, about, created_at], [event_id, pubkey, name, picture, about, created_at]
); );
} }
@@ -233,8 +220,8 @@ export async function updateChannelMetadata(event_id: string, value: string) {
const data = JSON.parse(value); const data = JSON.parse(value);
return await db.execute( return await db.execute(
"UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;", 'UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;',
[data.name, data.picture, data.about, event_id], [data.name, data.picture, data.about, event_id]
); );
} }
@@ -246,12 +233,12 @@ export async function createChannelMessage(
kind: number, kind: number,
content: string, content: string,
tags: string[][], tags: string[][],
created_at: number, created_at: number
) { ) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(
"INSERT OR IGNORE INTO channel_messages (channel_id, event_id, pubkey, kind, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);", 'INSERT OR IGNORE INTO channel_messages (channel_id, event_id, pubkey, kind, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);',
[channel_id, event_id, pubkey, kind, content, tags, created_at], [channel_id, event_id, pubkey, kind, content, tags, created_at]
); );
} }
@@ -259,7 +246,7 @@ export async function createChannelMessage(
export async function getChannelMessages(channel_id: string) { export async function getChannelMessages(channel_id: string) {
const db = await connect(); const db = await connect();
return await db.select( return await db.select(
`SELECT * FROM channel_messages WHERE channel_id = "${channel_id}" ORDER BY created_at ASC;`, `SELECT * FROM channel_messages WHERE channel_id = "${channel_id}" ORDER BY created_at ASC;`
); );
} }
@@ -267,7 +254,7 @@ export async function getChannelMessages(channel_id: string) {
export async function getChannelUsers(channel_id: string) { export async function getChannelUsers(channel_id: string) {
const db = await connect(); const db = await connect();
const result: any = await db.select( const result: any = await db.select(
`SELECT DISTINCT pubkey FROM channel_messages WHERE channel_id = "${channel_id}";`, `SELECT DISTINCT pubkey FROM channel_messages WHERE channel_id = "${channel_id}";`
); );
return result; return result;
} }
@@ -276,33 +263,29 @@ export async function getChannelUsers(channel_id: string) {
export async function getChatsByPubkey(pubkey: string) { export async function getChatsByPubkey(pubkey: string) {
const db = await connect(); const db = await connect();
const result: any = await db.select( const result: any = await db.select(
`SELECT DISTINCT sender_pubkey FROM chats WHERE receiver_pubkey = "${pubkey}" ORDER BY created_at DESC;`, `SELECT DISTINCT sender_pubkey FROM chats WHERE receiver_pubkey = "${pubkey}" ORDER BY created_at DESC;`
); );
const newArr: any = result.map((v) => ({ ...v, new_messages: 0 })); const newArr: any = result.map((v) => ({ ...v, new_messages: 0 }));
return newArr; return newArr;
} }
// get chat messages // get chat messages
export async function getChatMessages( export async function getChatMessages(receiver_pubkey: string, sender_pubkey: string) {
receiver_pubkey: string,
sender_pubkey: string,
) {
const db = await connect(); const db = await connect();
let receiver = []; let receiver = [];
const sender: any = await db.select( const sender: any = await db.select(
`SELECT * FROM chats WHERE sender_pubkey = "${sender_pubkey}" AND receiver_pubkey = "${receiver_pubkey}";`, `SELECT * FROM chats WHERE sender_pubkey = "${sender_pubkey}" AND receiver_pubkey = "${receiver_pubkey}";`
); );
if (receiver_pubkey !== sender_pubkey) { if (receiver_pubkey !== sender_pubkey) {
receiver = await db.select( receiver = await db.select(
`SELECT * FROM chats WHERE sender_pubkey = "${receiver_pubkey}" AND receiver_pubkey = "${sender_pubkey}";`, `SELECT * FROM chats WHERE sender_pubkey = "${receiver_pubkey}" AND receiver_pubkey = "${sender_pubkey}";`
); );
} }
const result = [...sender, ...receiver].sort( const result = [...sender, ...receiver].sort(
(x: { created_at: number }, y: { created_at: number }) => (x: { created_at: number }, y: { created_at: number }) => x.created_at - y.created_at
x.created_at - y.created_at,
); );
return result; return result;
@@ -315,12 +298,12 @@ export async function createChat(
sender_pubkey: string, sender_pubkey: string,
content: string, content: string,
tags: string[][], tags: string[][],
created_at: number, created_at: number
) { ) {
const db = await connect(); const db = await connect();
await db.execute( await db.execute(
"INSERT OR IGNORE INTO chats (event_id, receiver_pubkey, sender_pubkey, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?);", 'INSERT OR IGNORE INTO chats (event_id, receiver_pubkey, sender_pubkey, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?);',
[event_id, receiver_pubkey, sender_pubkey, content, tags, created_at], [event_id, receiver_pubkey, sender_pubkey, content, tags, created_at]
); );
return sender_pubkey; return sender_pubkey;
} }
@@ -328,26 +311,20 @@ export async function createChat(
// get setting // get setting
export async function getSetting(key: string) { export async function getSetting(key: string) {
const db = await connect(); const db = await connect();
const result = await db.select( const result = await db.select(`SELECT value FROM settings WHERE key = "${key}";`);
`SELECT value FROM settings WHERE key = "${key}";`,
);
return result[0]?.value; return result[0]?.value;
} }
// update setting // update setting
export async function updateSetting(key: string, value: string | number) { export async function updateSetting(key: string, value: string | number) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(`UPDATE settings SET value = "${value}" WHERE key = "${key}";`);
`UPDATE settings SET value = "${value}" WHERE key = "${key}";`,
);
} }
// get last login // get last login
export async function getLastLogin() { export async function getLastLogin() {
const db = await connect(); const db = await connect();
const result = await db.select( const result = await db.select(`SELECT value FROM settings WHERE key = "last_login";`);
`SELECT value FROM settings WHERE key = "last_login";`,
);
if (result[0]) { if (result[0]) {
return parseInt(result[0].value); return parseInt(result[0].value);
} else { } else {
@@ -359,7 +336,7 @@ export async function getLastLogin() {
export async function updateLastLogin(value: number) { export async function updateLastLogin(value: number) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(
`UPDATE settings SET value = ${value} WHERE key = "last_login";`, `UPDATE settings SET value = ${value} WHERE key = "last_login";`
); );
} }
@@ -367,7 +344,7 @@ export async function updateLastLogin(value: number) {
export async function getBlacklist(account_id: number, kind: number) { export async function getBlacklist(account_id: number, kind: number) {
const db = await connect(); const db = await connect();
return await db.select( return await db.select(
`SELECT * FROM blacklist WHERE account_id = "${account_id}" AND kind = "${kind}";`, `SELECT * FROM blacklist WHERE account_id = "${account_id}" AND kind = "${kind}";`
); );
} }
@@ -375,7 +352,7 @@ export async function getBlacklist(account_id: number, kind: number) {
export async function getActiveBlacklist(account_id: number, kind: number) { export async function getActiveBlacklist(account_id: number, kind: number) {
const db = await connect(); const db = await connect();
return await db.select( return await db.select(
`SELECT content FROM blacklist WHERE account_id = "${account_id}" AND kind = "${kind}" AND status = 1;`, `SELECT content FROM blacklist WHERE account_id = "${account_id}" AND kind = "${kind}" AND status = 1;`
); );
} }
@@ -384,12 +361,12 @@ export async function addToBlacklist(
account_id: number, account_id: number,
content: string, content: string,
kind: number, kind: number,
status?: number, status?: number
) { ) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(
"INSERT OR IGNORE INTO blacklist (account_id, content, kind, status) VALUES (?, ?, ?, ?);", 'INSERT OR IGNORE INTO blacklist (account_id, content, kind, status) VALUES (?, ?, ?, ?);',
[account_id, content, kind, status || 1], [account_id, content, kind, status || 1]
); );
} }
@@ -397,7 +374,7 @@ export async function addToBlacklist(
export async function updateItemInBlacklist(content: string, status: number) { export async function updateItemInBlacklist(content: string, status: number) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(
`UPDATE blacklist SET status = "${status}" WHERE content = "${content}";`, `UPDATE blacklist SET status = "${status}" WHERE content = "${content}";`
); );
} }
@@ -406,7 +383,7 @@ export async function getBlocks() {
const db = await connect(); const db = await connect();
const activeAccount = await getActiveAccount(); const activeAccount = await getActiveAccount();
const result: any = await db.select( const result: any = await db.select(
`SELECT * FROM blocks WHERE account_id = "${activeAccount.id}" ORDER BY created_at DESC;`, `SELECT * FROM blocks WHERE account_id = "${activeAccount.id}" ORDER BY created_at DESC;`
); );
return result; return result;
} }
@@ -416,8 +393,8 @@ export async function createBlock(kind: number, title: string, content: any) {
const db = await connect(); const db = await connect();
const activeAccount = await getActiveAccount(); const activeAccount = await getActiveAccount();
return await db.execute( return await db.execute(
"INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);", 'INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);',
[activeAccount.id, kind, title, content], [activeAccount.id, kind, title, content]
); );
} }
@@ -431,11 +408,11 @@ export async function removeBlock(id: string) {
export async function removeAll() { export async function removeAll() {
const db = await connect(); const db = await connect();
await db.execute(`UPDATE settings SET value = "0" WHERE key = "last_login";`); await db.execute(`UPDATE settings SET value = "0" WHERE key = "last_login";`);
await db.execute("DELETE FROM replies;"); await db.execute('DELETE FROM replies;');
await db.execute("DELETE FROM notes;"); await db.execute('DELETE FROM notes;');
await db.execute("DELETE FROM blacklist;"); await db.execute('DELETE FROM blacklist;');
await db.execute("DELETE FROM blocks;"); await db.execute('DELETE FROM blocks;');
await db.execute("DELETE FROM chats;"); await db.execute('DELETE FROM chats;');
await db.execute("DELETE FROM accounts;"); await db.execute('DELETE FROM accounts;');
return true; return true;
} }

View File

@@ -1,10 +1,13 @@
import App from "./app"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getSetting } from "@libs/storage"; import { createRoot } from 'react-dom/client';
import { RelayProvider } from "@shared/relayProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoot } from "react-dom/client";
const cacheTime = await getSetting("cache_time"); import { getSetting } from '@libs/storage';
import { RelayProvider } from '@shared/relayProvider';
import App from './app';
const cacheTime = await getSetting('cache_time');
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -14,7 +17,7 @@ const queryClient = new QueryClient({
}, },
}); });
const container = document.getElementById("root"); const container = document.getElementById('root');
const root = createRoot(container); const root = createRoot(container);
root.render( root.render(
@@ -22,5 +25,5 @@ root.render(
<RelayProvider> <RelayProvider>
<App /> <App />
</RelayProvider> </RelayProvider>
</QueryClientProvider>, </QueryClientProvider>
); );

View File

@@ -1,14 +1,18 @@
import { createChat, getLastLogin } from "@libs/storage"; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Image } from "@shared/image"; import { produce } from 'immer';
import { NetworkStatusIndicator } from "@shared/networkStatusIndicator"; import { useContext, useEffect } from 'react';
import { RelayContext } from "@shared/relayProvider"; import { Link } from 'react-router-dom';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { createChat, getLastLogin } from '@libs/storage';
import { useProfile } from "@utils/hooks/useProfile";
import { sendNativeNotification } from "@utils/notification"; import { Image } from '@shared/image';
import { produce } from "immer"; import { NetworkStatusIndicator } from '@shared/networkStatusIndicator';
import { useContext, useEffect } from "react"; import { RelayContext } from '@shared/relayProvider';
import { Link } from "react-router-dom";
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { sendNativeNotification } from '@utils/notification';
const lastLogin = await getLastLogin(); const lastLogin = await getLastLogin();
@@ -26,23 +30,22 @@ export function ActiveAccount({ data }: { data: any }) {
data.sender_pubkey, data.sender_pubkey,
data.content, data.content,
data.tags, data.tags,
data.created_at, data.created_at
); );
}, },
onSuccess: (data: any) => { onSuccess: (data: any) => {
const prev = queryClient.getQueryData(["chats"]); const prev = queryClient.getQueryData(['chats']);
const next = produce(prev, (draft: any) => { const next = produce(prev, (draft: any) => {
const target = draft.findIndex( const target = draft.findIndex(
(m: { sender_pubkey: string }) => m.sender_pubkey === data, (m: { sender_pubkey: string }) => m.sender_pubkey === data
); );
if (target !== -1) { if (target !== -1) {
draft[target]["new_messages"] = draft[target]['new_messages'] = draft[target]['new_messages'] + 1 || 1;
draft[target]["new_messages"] + 1 || 1;
} else { } else {
draft.push({ sender_pubkey: data, new_messages: 1 }); draft.push({ sender_pubkey: data, new_messages: 1 });
} }
}); });
queryClient.setQueryData(["chats"], next); queryClient.setQueryData(['chats'], next);
}, },
}); });
@@ -51,15 +54,15 @@ export function ActiveAccount({ data }: { data: any }) {
const sub = ndk.subscribe( const sub = ndk.subscribe(
{ {
kinds: [4], kinds: [4],
"#p": [data.pubkey], '#p': [data.pubkey],
since: since, since: since,
}, },
{ {
closeOnEose: false, closeOnEose: false,
}, }
); );
sub.addListener("event", (event) => { sub.addListener('event', (event) => {
switch (event.kind) { switch (event.kind) {
case 4: case 4:
// update state // update state
@@ -84,15 +87,12 @@ export function ActiveAccount({ data }: { data: any }) {
}; };
}, []); }, []);
if (status === "loading") { if (status === 'loading') {
return <div className="w-9 h-9 rounded-md bg-zinc-800 animate-pulse" />; return <div className="h-9 w-9 animate-pulse rounded-md bg-zinc-800" />;
} }
return ( return (
<Link <Link to={`/app/user/${data.pubkey}`} className="relative inline-block h-9 w-9">
to={`/app/user/${data.pubkey}`}
className="relative inline-block h-9 w-9"
>
<Image <Image
src={user.image} src={user.image}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}

View File

@@ -1,6 +1,8 @@
import { Image } from "@shared/image"; import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
export function InactiveAccount({ data }: { data: any }) { export function InactiveAccount({ data }: { data: any }) {
const { user } = useProfile(data.npub); const { user } = useProfile(data.npub);

View File

@@ -1,5 +1,6 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons"; import { useNavigate } from 'react-router-dom';
import { useNavigate } from "react-router-dom";
import { ArrowLeftIcon, ArrowRightIcon } from '@shared/icons';
export function AppHeader({ reverse }: { reverse?: boolean }) { export function AppHeader({ reverse }: { reverse?: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -15,8 +16,8 @@ export function AppHeader({ reverse }: { reverse?: boolean }) {
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
className={`shrink-0 flex h-11 w-full px-3 border-b border-zinc-900 items-center ${ className={`flex h-11 w-full shrink-0 items-center border-b border-zinc-900 px-3 ${
reverse ? "justify-start" : "justify-end" reverse ? 'justify-start' : 'justify-end'
}`} }`}
> >
<div className="flex gap-2.5"> <div className="flex gap-2.5">

View File

@@ -1,13 +1,14 @@
import { Navigation } from "@shared/navigation"; import { Outlet, ScrollRestoration } from 'react-router-dom';
import { Outlet, ScrollRestoration } from "react-router-dom";
import { Navigation } from '@shared/navigation';
export function AppLayout() { export function AppLayout() {
return ( return (
<div className="flex w-screen h-screen"> <div className="flex h-screen w-screen">
<div className="relative flex flex-row shrink-0"> <div className="relative flex shrink-0 flex-row">
<Navigation /> <Navigation />
</div> </div>
<div className="w-full h-full"> <div className="h-full w-full">
<Outlet /> <Outlet />
<ScrollRestoration /> <ScrollRestoration />
</div> </div>

View File

@@ -1,6 +1,7 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons"; import { platform } from '@tauri-apps/api/os';
import { platform } from "@tauri-apps/api/os"; import { Outlet, useNavigate } from 'react-router-dom';
import { Outlet, useNavigate } from "react-router-dom";
import { ArrowLeftIcon, ArrowRightIcon } from '@shared/icons';
const platformName = await platform(); const platformName = await platform();
@@ -28,7 +29,7 @@ export function AuthLayout() {
> >
<div <div
className={`flex h-full items-center gap-2 ${ className={`flex h-full items-center gap-2 ${
platformName === "darwin" ? "pl-[68px]" : "" platformName === 'darwin' ? 'pl-[68px]' : ''
}`} }`}
> >
<button <button

View File

@@ -1,8 +1,10 @@
import { LoaderIcon, PlusIcon } from "@shared/icons"; import { open } from '@tauri-apps/api/dialog';
import { open } from "@tauri-apps/api/dialog"; import { Body, fetch } from '@tauri-apps/api/http';
import { Body, fetch } from "@tauri-apps/api/http"; import { useState } from 'react';
import { createBlobFromFile } from "@utils/createBlobFromFile";
import { useState } from "react"; import { LoaderIcon, PlusIcon } from '@shared/icons';
import { createBlobFromFile } from '@utils/createBlobFromFile';
export function AvatarUploader({ setPicture }: { setPicture: any }) { export function AvatarUploader({ setPicture }: { setPicture: any }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -12,8 +14,8 @@ export function AvatarUploader({ setPicture }: { setPicture: any }) {
multiple: false, multiple: false,
filters: [ filters: [
{ {
name: "Image", name: 'Image',
extensions: ["png", "jpeg", "jpg", "gif"], extensions: ['png', 'jpeg', 'jpg', 'gif'],
}, },
], ],
}); });
@@ -24,24 +26,24 @@ export function AvatarUploader({ setPicture }: { setPicture: any }) {
} else { } else {
setLoading(true); setLoading(true);
const filename = selected.split("/").pop(); const filename = selected.split('/').pop();
const file = await createBlobFromFile(selected); const file = await createBlobFromFile(selected);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
const res: { data: { file: { id: string } } } = await fetch( const res: { data: { file: { id: string } } } = await fetch(
"https://void.cat/upload?cli=false", 'https://void.cat/upload?cli=false',
{ {
method: "POST", method: 'POST',
timeout: 5, timeout: 5,
headers: { headers: {
accept: "*/*", accept: '*/*',
"Content-Type": "application/octet-stream", 'Content-Type': 'application/octet-stream',
"V-Filename": filename, 'V-Filename': filename,
"V-Description": "Upload from https://lume.nu", 'V-Description': 'Upload from https://lume.nu',
"V-Strip-Metadata": "true", 'V-Strip-Metadata': 'true',
}, },
body: Body.bytes(buf), body: Body.bytes(buf),
}, }
); );
const image = `https://void.cat/d/${res.data.file.id}.jpg`; const image = `https://void.cat/d/${res.data.file.id}.jpg`;
@@ -57,7 +59,7 @@ export function AvatarUploader({ setPicture }: { setPicture: any }) {
<button <button
type="button" type="button"
onClick={() => openFileDialog()} onClick={() => openFileDialog()}
className="w-full h-full inline-flex items-center justify-center bg-zinc-900/40" className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
> >
{loading ? ( {loading ? (
<LoaderIcon className="h-6 w-6 animate-spin text-zinc-100" /> <LoaderIcon className="h-6 w-6 animate-spin text-zinc-100" />

View File

@@ -1,8 +1,10 @@
import { LoaderIcon, PlusIcon } from "@shared/icons"; import { open } from '@tauri-apps/api/dialog';
import { open } from "@tauri-apps/api/dialog"; import { Body, fetch } from '@tauri-apps/api/http';
import { Body, fetch } from "@tauri-apps/api/http"; import { useState } from 'react';
import { createBlobFromFile } from "@utils/createBlobFromFile";
import { useState } from "react"; import { LoaderIcon, PlusIcon } from '@shared/icons';
import { createBlobFromFile } from '@utils/createBlobFromFile';
export function BannerUploader({ setBanner }: { setBanner: any }) { export function BannerUploader({ setBanner }: { setBanner: any }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -12,8 +14,8 @@ export function BannerUploader({ setBanner }: { setBanner: any }) {
multiple: false, multiple: false,
filters: [ filters: [
{ {
name: "Image", name: 'Image',
extensions: ["png", "jpeg", "jpg", "gif"], extensions: ['png', 'jpeg', 'jpg', 'gif'],
}, },
], ],
}); });
@@ -24,24 +26,24 @@ export function BannerUploader({ setBanner }: { setBanner: any }) {
} else { } else {
setLoading(true); setLoading(true);
const filename = selected.split("/").pop(); const filename = selected.split('/').pop();
const file = await createBlobFromFile(selected); const file = await createBlobFromFile(selected);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
const res: { data: { file: { id: string } } } = await fetch( const res: { data: { file: { id: string } } } = await fetch(
"https://void.cat/upload?cli=false", 'https://void.cat/upload?cli=false',
{ {
method: "POST", method: 'POST',
timeout: 5, timeout: 5,
headers: { headers: {
accept: "*/*", accept: '*/*',
"Content-Type": "application/octet-stream", 'Content-Type': 'application/octet-stream',
"V-Filename": filename, 'V-Filename': filename,
"V-Description": "Upload from https://lume.nu", 'V-Description': 'Upload from https://lume.nu',
"V-Strip-Metadata": "true", 'V-Strip-Metadata': 'true',
}, },
body: Body.bytes(buf), body: Body.bytes(buf),
}, }
); );
const image = `https://void.cat/d/${res.data.file.id}.jpg`; const image = `https://void.cat/d/${res.data.file.id}.jpg`;
@@ -57,7 +59,7 @@ export function BannerUploader({ setBanner }: { setBanner: any }) {
<button <button
type="button" type="button"
onClick={() => openFileDialog()} onClick={() => openFileDialog()}
className="w-full h-full inline-flex items-center justify-center bg-zinc-900/40" className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
> >
{loading ? ( {loading ? (
<LoaderIcon className="h-8 w-8 animate-spin text-zinc-100" /> <LoaderIcon className="h-8 w-8 animate-spin text-zinc-100" />

View File

@@ -1,5 +1,5 @@
import { ReactNode } from "react"; import { ReactNode } from 'react';
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge';
export function Button({ export function Button({
preset, preset,
@@ -7,24 +7,24 @@ export function Button({
disabled = false, disabled = false,
onClick = undefined, onClick = undefined,
}: { }: {
preset: "small" | "publish" | "large"; preset: 'small' | 'publish' | 'large';
children: ReactNode; children: ReactNode;
disabled?: boolean; disabled?: boolean;
onClick?: () => void; onClick?: () => void;
}) { }) {
let preClass: string; let preClass: string;
switch (preset) { switch (preset) {
case "small": case 'small':
preClass = preClass =
"w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600"; 'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600';
break; break;
case "publish": case 'publish':
preClass = preClass =
"w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600"; 'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600';
break; break;
case "large": case 'large':
preClass = preClass =
"h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"; 'h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600';
break; break;
default: default:
break; break;
@@ -36,8 +36,8 @@ export function Button({
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className={twMerge( className={twMerge(
"inline-flex items-center justify-center gap-1 transform active:translate-y-1 disabled:pointer-events-none disabled:opacity-50 focus:outline-none", 'inline-flex transform items-center justify-center gap-1 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
preClass, preClass
)} )}
> >
{children} {children}

View File

@@ -1,42 +1,44 @@
import { PlusCircleIcon } from "@shared/icons"; import { open } from '@tauri-apps/api/dialog';
import { open } from "@tauri-apps/api/dialog"; import { listen } from '@tauri-apps/api/event';
import { listen } from "@tauri-apps/api/event"; import { Body, fetch } from '@tauri-apps/api/http';
import { Body, fetch } from "@tauri-apps/api/http"; import { useCallback, useEffect, useState } from 'react';
import { createBlobFromFile } from "@utils/createBlobFromFile"; import { Transforms } from 'slate';
import { useCallback, useEffect, useState } from "react"; import { useSlateStatic } from 'slate-react';
import { Transforms } from "slate";
import { useSlateStatic } from "slate-react"; import { PlusCircleIcon } from '@shared/icons';
import { createBlobFromFile } from '@utils/createBlobFromFile';
export function ImageUploader() { export function ImageUploader() {
const editor = useSlateStatic(); const editor = useSlateStatic();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const insertImage = (editor, url) => { const insertImage = (editor, url) => {
const image = { type: "image", url, children: [{ text: url }] }; const image = { type: 'image', url, children: [{ text: url }] };
Transforms.insertNodes(editor, image); Transforms.insertNodes(editor, image);
}; };
const uploadToVoidCat = useCallback( const uploadToVoidCat = useCallback(
async (filepath) => { async (filepath) => {
const filename = filepath.split("/").pop(); const filename = filepath.split('/').pop();
const file = await createBlobFromFile(filepath); const file = await createBlobFromFile(filepath);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
try { try {
const res: { data: { file: { id: string } } } = await fetch( const res: { data: { file: { id: string } } } = await fetch(
"https://void.cat/upload?cli=false", 'https://void.cat/upload?cli=false',
{ {
method: "POST", method: 'POST',
timeout: 5, timeout: 5,
headers: { headers: {
accept: "*/*", accept: '*/*',
"Content-Type": "application/octet-stream", 'Content-Type': 'application/octet-stream',
"V-Filename": filename, 'V-Filename': filename,
"V-Description": "Uploaded from https://lume.nu", 'V-Description': 'Uploaded from https://lume.nu',
"V-Strip-Metadata": "true", 'V-Strip-Metadata': 'true',
}, },
body: Body.bytes(buf), body: Body.bytes(buf),
}, }
); );
const image = `https://void.cat/d/${res.data.file.id}.webp`; const image = `https://void.cat/d/${res.data.file.id}.webp`;
// update parent state // update parent state
@@ -49,13 +51,13 @@ export function ImageUploader() {
// handle error // handle error
if (error instanceof SyntaxError) { if (error instanceof SyntaxError) {
// Unexpected token < in JSON // Unexpected token < in JSON
console.log("There was a SyntaxError", error); console.log('There was a SyntaxError', error);
} else { } else {
console.log("There was an error", error); console.log('There was an error', error);
} }
} }
}, },
[editor], [editor]
); );
const openFileDialog = async () => { const openFileDialog = async () => {
@@ -63,8 +65,8 @@ export function ImageUploader() {
multiple: false, multiple: false,
filters: [ filters: [
{ {
name: "Image", name: 'Image',
extensions: ["png", "jpeg", "jpg", "gif"], extensions: ['png', 'jpeg', 'jpg', 'gif'],
}, },
], ],
}); });
@@ -81,7 +83,7 @@ export function ImageUploader() {
useEffect(() => { useEffect(() => {
async function initFileDrop() { async function initFileDrop() {
const unlisten = await listen("tauri://file-drop", (event) => { const unlisten = await listen('tauri://file-drop', (event) => {
// set loading state // set loading state
setLoading(true); setLoading(true);
// upload file // upload file

View File

@@ -1,26 +1,26 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from '@headlessui/react';
import { Button } from "@shared/button"; import { Fragment } from 'react';
import { Post } from "@shared/composer/types/post"; import { useHotkeys } from 'react-hotkeys-hook';
import { User } from "@shared/composer/user";
import { Button } from '@shared/button';
import { Post } from '@shared/composer/types/post';
import { User } from '@shared/composer/user';
import { import {
CancelIcon, CancelIcon,
ChevronDownIcon, ChevronDownIcon,
ChevronRightIcon, ChevronRightIcon,
ComposeIcon, ComposeIcon,
} from "@shared/icons"; } from '@shared/icons';
import { useComposer } from "@stores/composer";
import { COMPOSE_SHORTCUT } from "@stores/shortcuts"; import { useComposer } from '@stores/composer';
import { useAccount } from "@utils/hooks/useAccount"; import { COMPOSE_SHORTCUT } from '@stores/shortcuts';
import { Fragment } from "react";
import { useHotkeys } from "react-hotkeys-hook"; import { useAccount } from '@utils/hooks/useAccount';
export function Composer() { export function Composer() {
const { account } = useAccount(); const { account } = useAccount();
const [toggle, open] = useComposer((state) => [ const [toggle, open] = useComposer((state) => [state.toggleModal, state.open]);
state.toggleModal,
state.open,
]);
const closeModal = () => { const closeModal = () => {
toggle(false); toggle(false);
@@ -76,13 +76,11 @@ export function Composer() {
<div <div
onClick={closeModal} onClick={closeModal}
onKeyDown={closeModal} onKeyDown={closeModal}
role="button"
tabIndex={0}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
> >
<CancelIcon <CancelIcon width={16} height={16} className="text-zinc-500" />
width={16}
height={16}
className="text-zinc-500"
/>
</div> </div>
</div> </div>
{account && <Post />} {account && <Post />}

View File

@@ -1,26 +1,23 @@
import { usePublish } from "@libs/ndk"; import { useCallback, useMemo, useState } from 'react';
import { Button } from "@shared/button"; import { Node, Transforms, createEditor } from 'slate';
import { ImageUploader } from "@shared/composer/imageUploader"; import { withHistory } from 'slate-history';
import { TrashIcon } from "@shared/icons"; import { Editable, ReactEditor, Slate, useSlateStatic, withReact } from 'slate-react';
import { MentionNote } from "@shared/notes/mentions/note";
import { useComposer } from "@stores/composer"; import { usePublish } from '@libs/ndk';
import { FULL_RELAYS } from "@stores/constants";
import { useCallback, useMemo, useState } from "react"; import { Button } from '@shared/button';
import { Node, Transforms, createEditor } from "slate"; import { ImageUploader } from '@shared/composer/imageUploader';
import { withHistory } from "slate-history"; import { TrashIcon } from '@shared/icons';
import { import { MentionNote } from '@shared/notes/mentions/note';
Editable,
ReactEditor, import { useComposer } from '@stores/composer';
Slate, import { FULL_RELAYS } from '@stores/constants';
useSlateStatic,
withReact,
} from "slate-react";
const withImages = (editor) => { const withImages = (editor) => {
const { isVoid } = editor; const { isVoid } = editor;
editor.isVoid = (element) => { editor.isVoid = (element) => {
return element.type === "image" ? true : isVoid(element); return element.type === 'image' ? true : isVoid(element);
}; };
return editor; return editor;
@@ -50,7 +47,7 @@ const ImagePreview = ({
<button <button
type="button" type="button"
onClick={() => Transforms.removeNodes(editor, { at: path })} onClick={() => Transforms.removeNodes(editor, { at: path })}
className="absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center gap-0.5 rounded bg-zinc-800 text-base font-medium text-zinc-400 shadow-mini-button hover:bg-zinc-700" className="shadow-mini-button absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center gap-0.5 rounded bg-zinc-800 text-base font-medium text-zinc-400 hover:bg-zinc-700"
> >
<TrashIcon width={14} height={14} className="text-zinc-100" /> <TrashIcon width={14} height={14} className="text-zinc-100" />
</button> </button>
@@ -61,10 +58,7 @@ const ImagePreview = ({
export function Post() { export function Post() {
const publish = usePublish(); const publish = usePublish();
const editor = useMemo( const editor = useMemo(() => withReact(withImages(withHistory(createEditor()))), []);
() => withReact(withImages(withHistory(createEditor()))),
[],
);
const [repost, reply, toggle] = useComposer((state) => [ const [repost, reply, toggle] = useComposer((state) => [
state.repost, state.repost,
@@ -75,14 +69,14 @@ export function Post() {
{ {
children: [ children: [
{ {
text: "", text: '',
}, },
], ],
}, },
]); ]);
const serialize = useCallback((nodes: Node[]) => { const serialize = useCallback((nodes: Node[]) => {
return nodes.map((n) => Node.string(n)).join("\n"); return nodes.map((n) => Node.string(n)).join('\n');
}, []); }, []);
const getRef = () => { const getRef = () => {
@@ -104,21 +98,21 @@ export function Post() {
if (repost.id && repost.pubkey) { if (repost.id && repost.pubkey) {
kind = 6; kind = 6;
tags = [ tags = [
["e", repost.id, FULL_RELAYS[0], "root"], ['e', repost.id, FULL_RELAYS[0], 'root'],
["p", repost.pubkey], ['p', repost.pubkey],
]; ];
} else if (reply.id && reply.pubkey) { } else if (reply.id && reply.pubkey) {
kind = 1; kind = 1;
if (reply.root && reply.root !== reply.id) { if (reply.root && reply.root !== reply.id) {
tags = [ tags = [
["e", reply.id, FULL_RELAYS[0], "root"], ['e', reply.id, FULL_RELAYS[0], 'root'],
["e", reply.root, FULL_RELAYS[0], "reply"], ['e', reply.root, FULL_RELAYS[0], 'reply'],
["p", reply.pubkey], ['p', reply.pubkey],
]; ];
} else { } else {
tags = [ tags = [
["e", reply.id, FULL_RELAYS[0], "root"], ['e', reply.id, FULL_RELAYS[0], 'root'],
["p", reply.pubkey], ['p', reply.pubkey],
]; ];
} }
} else { } else {
@@ -138,10 +132,11 @@ export function Post() {
const renderElement = useCallback((props: any) => { const renderElement = useCallback((props: any) => {
switch (props.element.type) { switch (props.element.type) {
case "image": case 'image':
if (props.element.url) { if (props.element.url) {
return <ImagePreview {...props} />; return <ImagePreview {...props} />;
} }
break;
default: default:
return <p {...props.attributes}>{props.children}</p>; return <p {...props.attributes}>{props.children}</p>;
} }
@@ -156,12 +151,9 @@ export function Post() {
</div> </div>
<div className="w-full"> <div className="w-full">
<Editable <Editable
autoFocus placeholder={refID ? 'Share your thoughts on it' : "What's on your mind?"}
placeholder={
refID ? "Share your thoughts on it" : "What's on your mind?"
}
spellCheck="false" spellCheck="false"
className={`${refID ? "!min-h-42" : "!min-h-[86px]"} markdown`} className={`${refID ? '!min-h-42' : '!min-h-[86px]'} markdown`}
renderElement={renderElement} renderElement={renderElement}
/> />
{refID && <MentionNote id={refID} />} {refID && <MentionNote id={refID} />}

View File

@@ -1,6 +1,8 @@
import { Image } from "@shared/image"; import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
export function User({ pubkey }: { pubkey: string }) { export function User({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);

View File

@@ -1,21 +1,20 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from '@headlessui/react';
import { usePublish } from "@libs/ndk"; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { NDKEvent } from "@nostr-dev-kit/ndk"; import { useQueryClient } from '@tanstack/react-query';
import { AvatarUploader } from "@shared/avatarUploader"; import { fetch } from '@tauri-apps/api/http';
import { BannerUploader } from "@shared/bannerUploader"; import { Fragment, useEffect, useState } from 'react';
import { import { useForm } from 'react-hook-form';
CancelIcon,
CheckCircleIcon, import { usePublish } from '@libs/ndk';
LoaderIcon,
UnverifiedIcon, import { AvatarUploader } from '@shared/avatarUploader';
} from "@shared/icons"; import { BannerUploader } from '@shared/bannerUploader';
import { Image } from "@shared/image"; import { CancelIcon, CheckCircleIcon, LoaderIcon, UnverifiedIcon } from '@shared/icons';
import { DEFAULT_AVATAR } from "@stores/constants"; import { Image } from '@shared/image';
import { useQueryClient } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/api/http"; import { DEFAULT_AVATAR } from '@stores/constants';
import { useAccount } from "@utils/hooks/useAccount";
import { Fragment, useEffect, useState } from "react"; import { useAccount } from '@utils/hooks/useAccount';
import { useForm } from "react-hook-form";
export function EditProfileModal() { export function EditProfileModal() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -24,8 +23,8 @@ export function EditProfileModal() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [picture, setPicture] = useState(DEFAULT_AVATAR); const [picture, setPicture] = useState(DEFAULT_AVATAR);
const [banner, setBanner] = useState(""); const [banner, setBanner] = useState('');
const [nip05, setNIP05] = useState({ verified: false, text: "" }); const [nip05, setNIP05] = useState({ verified: false, text: '' });
const { account } = useAccount(); const { account } = useAccount();
const { const {
@@ -36,7 +35,7 @@ export function EditProfileModal() {
formState: { isValid, errors }, formState: { isValid, errors },
} = useForm({ } = useForm({
defaultValues: async () => { defaultValues: async () => {
const res: any = queryClient.getQueryData(["user", account.pubkey]); const res: any = queryClient.getQueryData(['user', account.pubkey]);
if (res.image) { if (res.image) {
setPicture(res.image); setPicture(res.image);
} }
@@ -60,16 +59,16 @@ export function EditProfileModal() {
const verifyNIP05 = async (data: string) => { const verifyNIP05 = async (data: string) => {
if (data) { if (data) {
const url = data.split("@"); const url = data.split('@');
const username = url[0]; const username = url[0];
const service = url[1]; const service = url[1];
const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`; const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`;
const res: any = await fetch(verifyURL, { const res: any = await fetch(verifyURL, {
method: "GET", method: 'GET',
timeout: 30, timeout: 30,
headers: { headers: {
"Content-Type": "application/json; charset=utf-8", 'Content-Type': 'application/json; charset=utf-8',
}, },
}); });
@@ -107,8 +106,8 @@ export function EditProfileModal() {
}); });
} else { } else {
setNIP05((prev) => ({ ...prev, verified: false })); setNIP05((prev) => ({ ...prev, verified: false }));
setError("nip05", { setError('nip05', {
type: "manual", type: 'manual',
message: "Can't verify your Lume ID / NIP-05, please check again", message: "Can't verify your Lume ID / NIP-05, please check again",
}); });
} }
@@ -123,7 +122,7 @@ export function EditProfileModal() {
if (event.id) { if (event.id) {
setTimeout(() => { setTimeout(() => {
// invalid cache // invalid cache
queryClient.invalidateQueries(["user", account.pubkey]); queryClient.invalidateQueries(['user', account.pubkey]);
// reset form // reset form
reset(); reset();
// reset state // reset state
@@ -148,7 +147,7 @@ export function EditProfileModal() {
<button <button
type="button" type="button"
onClick={() => openModal()} onClick={() => openModal()}
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium" className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
> >
Edit profile Edit profile
</button> </button>
@@ -189,45 +188,45 @@ export function EditProfileModal() {
onClick={closeModal} onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
> >
<CancelIcon className="w-5 h-5 text-zinc-300" /> <CancelIcon className="h-5 w-5 text-zinc-300" />
</button> </button>
</div> </div>
</div> </div>
<div className="flex h-full w-full flex-col overflow-y-auto"> <div className="flex h-full w-full flex-col overflow-y-auto">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0"> <form onSubmit={handleSubmit(onSubmit)} className="mb-0">
<input <input
type={"hidden"} type={'hidden'}
{...register("picture")} {...register('picture')}
value={picture} value={picture}
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-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<input <input
type={"hidden"} type={'hidden'}
{...register("banner")} {...register('banner')}
value={banner} value={banner}
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-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
<div className="relative"> <div className="relative">
<div className="relative w-full h-44 bg-zinc-800"> <div className="relative h-44 w-full bg-zinc-800">
<Image <Image
src={banner} src={banner}
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg" fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
alt="user's banner" alt="user's banner"
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full"> <div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
<BannerUploader setBanner={setBanner} /> <BannerUploader setBanner={setBanner} />
</div> </div>
</div> </div>
<div className="px-4 mb-5"> <div className="mb-5 px-4">
<div className="z-10 relative h-14 w-14 -mt-7"> <div className="relative z-10 -mt-7 h-14 w-14">
<Image <Image
src={picture} src={picture}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt="user's avatar" alt="user's avatar"
className="h-14 w-14 object-cover ring-2 ring-zinc-900 rounded-lg" className="h-14 w-14 rounded-lg object-cover ring-2 ring-zinc-900"
/> />
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full"> <div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
<AvatarUploader setPicture={setPicture} /> <AvatarUploader setPicture={setPicture} />
</div> </div>
</div> </div>
@@ -235,41 +234,47 @@ export function EditProfileModal() {
</div> </div>
<div className="flex flex-col gap-4 px-4 pb-4"> <div className="flex flex-col gap-4 px-4 pb-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="name"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Name Name
</label> </label>
<input <input
type={"text"} type={'text'}
{...register("name", { {...register('name', {
required: true, required: true,
minLength: 4, minLength: 4,
})} })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="nip05"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Lume ID / NIP-05 Lume ID / NIP-05
</label> </label>
<div className="relative"> <div className="relative">
<input <input
{...register("nip05", { {...register('nip05', {
required: true, required: true,
minLength: 4, minLength: 4,
})} })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
<div className="absolute top-1/2 right-2 transform -translate-y-1/2"> <div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
{nip05.verified ? ( {nip05.verified ? (
<span className="inline-flex items-center gap-1 rounded h-6 px-2 bg-green-500 text-sm font-medium"> <span className="inline-flex h-6 items-center gap-1 rounded bg-green-500 px-2 text-sm font-medium">
<CheckCircleIcon className="w-4 h-4 text-white" /> <CheckCircleIcon className="h-4 w-4 text-white" />
Verified Verified
</span> </span>
) : ( ) : (
<span className="inline-flex items-center gap-1 rounded h-6 px-2 bg-red-500 text-sm font-medium"> <span className="inline-flex h-6 items-center gap-1 rounded bg-red-500 px-2 text-sm font-medium">
<UnverifiedIcon className="w-4 h-4 text-white" /> <UnverifiedIcon className="h-4 w-4 text-white" />
Unverified Unverified
</span> </span>
)} )}
@@ -282,36 +287,42 @@ export function EditProfileModal() {
</div> </div>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="about"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Bio Bio
</label> </label>
<textarea <textarea
{...register("about")} {...register('about')}
spellCheck={false} spellCheck={false}
className="relative resize-none h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400"> <label
htmlFor="website"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
>
Website Website
</label> </label>
<input <input
type={"text"} type={'text'}
{...register("website", { required: false })} {...register('website', { required: false })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500" className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/> />
</div> </div>
<div> <div>
<button <button
type="submit" type="submit"
disabled={!isValid} disabled={!isValid}
className="inline-flex items-center justify-center gap-1 transform active:translate-y-1 disabled:pointer-events-none disabled:opacity-50 focus:outline-none h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600" className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
> >
{loading ? ( {loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" /> <LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : ( ) : (
"Update" 'Update'
)} )}
</button> </button>
</div> </div>

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function ArrowRightCircleIcon( export function ArrowRightCircleIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) { ) {
return ( return (
<svg <svg

View File

@@ -1,8 +1,6 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function BellIcon( export function BellIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

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

View File

@@ -1,7 +1,7 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function CheckCircleIcon( export function CheckCircleIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) { ) {
return ( return (
<svg <svg

View File

@@ -1,7 +1,7 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function ChevronDownIcon( export function ChevronDownIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) { ) {
return ( return (
<svg <svg

View File

@@ -1,7 +1,7 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function ChevronRightIcon( export function ChevronRightIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) { ) {
return ( return (
<svg <svg

View File

@@ -1,8 +1,6 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function CommandIcon( export function CommandIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,8 +1,6 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function ComposeIcon( export function ComposeIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,8 +1,6 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function CopyIcon( export function CopyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,8 +1,6 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function EditIcon( export function EditIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,8 +1,6 @@
import { SVGProps } from "react"; import { SVGProps } from 'react';
export function EmptyIcon( export function EmptyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -52,10 +50,7 @@ export function EmptyIcon(
> >
<feFlood floodOpacity="0" result="BackgroundImageFix" /> <feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur <feGaussianBlur result="effect1_foregroundBlur_110_63" stdDeviation="5.5" />
result="effect1_foregroundBlur_110_63"
stdDeviation="5.5"
/>
</filter> </filter>
<clipPath id="clip0_110_63"> <clipPath id="clip0_110_63">
<path fill="#fff" d="M0 0H120V120H0z" /> <path fill="#fff" d="M0 0H120V120H0z" />

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