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",
"tauri": "tauri",
"add-migrate": "cd src-tauri/ && sqlx migrate add",
"prepare": "husky install"
"prepare": "husky install",
"lint": "eslint ./src --fix",
"format": "prettier ./src --write"
},
"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": {
"@floating-ui/react": "^0.23.1",
@@ -48,20 +51,29 @@
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "^1.4.0",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/node": "^18.16.18",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@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",
"autoprefixer": "^10.4.14",
"cross-env": "^7.0.3",
"csstype": "^3.1.2",
"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",
"lint-staged": "^13.2.3",
"postcss": "^8.4.24",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"prop-types": "^15.8.1",
"rome": "12.1.0",
"tailwindcss": "^3.3.2",
"typescript": "^4.9.5",
"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 { AuthCreateScreen } from "@app/auth/create";
import { CreateStep1Screen } from "@app/auth/create/step-1";
import { CreateStep2Screen } from "@app/auth/create/step-2";
import { CreateStep3Screen } from "@app/auth/create/step-3";
import { CreateStep4Screen } from "@app/auth/create/step-4";
import { AuthImportScreen } from "@app/auth/import";
import { ImportStep1Screen } from "@app/auth/import/step-1";
import { ImportStep2Screen } from "@app/auth/import/step-2";
import { OnboardingScreen } from "@app/auth/onboarding";
import { WelcomeScreen } from "@app/auth/welcome";
import { ChannelScreen } from "@app/channel";
import { ChatScreen } from "@app/chat";
import { ErrorScreen } from "@app/error";
import { Root } from "@app/root";
import { AccountSettingsScreen } from "@app/settings/account";
import { GeneralSettingsScreen } from "@app/settings/general";
import { ShortcutsSettingsScreen } from "@app/settings/shortcuts";
import { SpaceScreen } from "@app/space";
import { TrendingScreen } from "@app/trending";
import { UserScreen } from "@app/user";
import { AppLayout } from "@shared/appLayout";
import { AuthLayout } from "@shared/authLayout";
import { Protected } from "@shared/protected";
import { SettingsLayout } from "@shared/settingsLayout";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { AuthCreateScreen } from '@app/auth/create';
import { CreateStep1Screen } from '@app/auth/create/step-1';
import { CreateStep2Screen } from '@app/auth/create/step-2';
import { CreateStep3Screen } from '@app/auth/create/step-3';
import { CreateStep4Screen } from '@app/auth/create/step-4';
import { AuthImportScreen } from '@app/auth/import';
import { ImportStep1Screen } from '@app/auth/import/step-1';
import { ImportStep2Screen } from '@app/auth/import/step-2';
import { OnboardingScreen } from '@app/auth/onboarding';
import { WelcomeScreen } from '@app/auth/welcome';
import { ChannelScreen } from '@app/channel';
import { ChatScreen } from '@app/chat';
import { ErrorScreen } from '@app/error';
import { Root } from '@app/root';
import { AccountSettingsScreen } from '@app/settings/account';
import { GeneralSettingsScreen } from '@app/settings/general';
import { ShortcutsSettingsScreen } from '@app/settings/shortcuts';
import { SpaceScreen } from '@app/space';
import { TrendingScreen } from '@app/trending';
import { UserScreen } from '@app/user';
import { AppLayout } from '@shared/appLayout';
import { AuthLayout } from '@shared/authLayout';
import { Protected } from '@shared/protected';
import { SettingsLayout } from '@shared/settingsLayout';
import './index.css';
const router = createBrowserRouter([
{
path: "/",
path: '/',
element: (
<Protected>
<Root />
@@ -36,57 +39,57 @@ const router = createBrowserRouter([
errorElement: <ErrorScreen />,
},
{
path: "/auth",
path: '/auth',
element: <AuthLayout />,
children: [
{ path: "welcome", element: <WelcomeScreen /> },
{ path: "onboarding", element: <OnboardingScreen /> },
{ path: 'welcome', element: <WelcomeScreen /> },
{ path: 'onboarding', element: <OnboardingScreen /> },
{
path: "import",
path: 'import',
element: <AuthImportScreen />,
children: [
{ path: "", element: <ImportStep1Screen /> },
{ path: "step-2", element: <ImportStep2Screen /> },
{ path: '', element: <ImportStep1Screen /> },
{ path: 'step-2', element: <ImportStep2Screen /> },
],
},
{
path: "create",
path: 'create',
element: <AuthCreateScreen />,
children: [
{ path: "", element: <CreateStep1Screen /> },
{ path: "step-2", element: <CreateStep2Screen /> },
{ path: "step-3", element: <CreateStep3Screen /> },
{ path: "step-4", element: <CreateStep4Screen /> },
{ path: '', element: <CreateStep1Screen /> },
{ path: 'step-2', element: <CreateStep2Screen /> },
{ path: 'step-3', element: <CreateStep3Screen /> },
{ path: 'step-4', element: <CreateStep4Screen /> },
],
},
],
},
{
path: "/app",
path: '/app',
element: (
<Protected>
<AppLayout />
</Protected>
),
children: [
{ path: "space", element: <SpaceScreen /> },
{ path: "trending", element: <TrendingScreen /> },
{ path: "user/:pubkey", element: <UserScreen /> },
{ path: "chat/:pubkey", element: <ChatScreen /> },
{ path: "channel/:id", element: <ChannelScreen /> },
{ path: 'space', element: <SpaceScreen /> },
{ path: 'trending', element: <TrendingScreen /> },
{ path: 'user/:pubkey', element: <UserScreen /> },
{ path: 'chat/:pubkey', element: <ChatScreen /> },
{ path: 'channel/:id', element: <ChannelScreen /> },
],
},
{
path: "/settings",
path: '/settings',
element: (
<Protected>
<SettingsLayout />
</Protected>
),
children: [
{ path: "general", element: <GeneralSettingsScreen /> },
{ path: "shortcuts", element: <ShortcutsSettingsScreen /> },
{ path: "account", element: <AccountSettingsScreen /> },
{ path: 'general', element: <GeneralSettingsScreen /> },
{ path: 'shortcuts', element: <ShortcutsSettingsScreen /> },
{ path: 'account', element: <AccountSettingsScreen /> },
],
},
]);

View File

@@ -1,21 +1,20 @@
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import { Image } from '@shared/image';
export function User({
pubkey,
fallback,
}: { pubkey: string; fallback?: string }) {
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }) {
const { status, user } = useProfile(pubkey, fallback);
if (status === "loading") {
if (status === 'loading') {
return (
<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">
<span className="w-1/2 h-4 rounded bg-zinc-800 animate-pulse" />
<span className="w-1/3 h-3 rounded bg-zinc-800 animate-pulse" />
<span className="h-4 w-1/2 animate-pulse rounded bg-zinc-800" />
<span className="h-3 w-1/3 animate-pulse rounded bg-zinc-800" />
</div>
</div>
);

View File

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

View File

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

View File

@@ -1,19 +1,21 @@
import { AvatarUploader } from "@shared/avatarUploader";
import { BannerUploader } from "@shared/bannerUploader";
import { LoaderIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useOnboarding } from "@stores/onboarding";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { AvatarUploader } from '@shared/avatarUploader';
import { BannerUploader } from '@shared/bannerUploader';
import { LoaderIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useOnboarding } from '@stores/onboarding';
export function CreateStep2Screen() {
const navigate = useNavigate();
const createProfile = useOnboarding((state: any) => state.createProfile);
const [picture, setPicture] = useState(DEFAULT_AVATAR);
const [banner, setBanner] = useState("");
const [banner, setBanner] = useState('');
const [loading, setLoading] = useState(false);
const {
@@ -33,57 +35,52 @@ export function CreateStep2Screen() {
};
createProfile(profile);
// redirect to next step
setTimeout(
() => navigate("/auth/create/step-3", { replace: true }),
1200,
);
setTimeout(() => navigate('/auth/create/step-3', { replace: true }), 1200);
} catch {
console.log("error");
console.log('error');
}
};
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">
Create your profile
</h1>
<h1 className="text-xl font-semibold text-zinc-100">Create your profile</h1>
</div>
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col mb-0">
<div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
<input
type={"hidden"}
{...register("picture")}
type={'hidden'}
{...register('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
type={"hidden"}
{...register("banner")}
type={'hidden'}
{...register('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 w-full h-44 bg-zinc-800">
<div className="relative h-44 w-full bg-zinc-800">
<Image
src={banner}
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
alt="user's banner"
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} />
</div>
</div>
<div className="px-4 mb-5">
<div className="z-10 relative h-14 w-14 -mt-7">
<div className="mb-5 px-4">
<div className="relative z-10 -mt-7 h-14 w-14">
<Image
src={picture}
fallback={DEFAULT_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} />
</div>
</div>
@@ -91,51 +88,60 @@ export function CreateStep2Screen() {
</div>
<div className="flex flex-col gap-4 px-4 pb-4">
<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 *
</label>
<input
type={"text"}
{...register("name", {
type={'text'}
{...register('name', {
required: true,
minLength: 4,
})}
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 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
</label>
<textarea
{...register("about")}
{...register('about')}
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 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
</label>
<input
type={"text"}
{...register("website", {
type={'text'}
{...register('website', {
required: 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>
<button
type="submit"
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 ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
"Continue →"
'Continue →'
)}
</button>
</div>

View File

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

View File

@@ -1,110 +1,114 @@
import { User } from "@app/auth/components/user";
import { updateAccount } from "@libs/storage";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CheckCircleIcon, LoaderIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAccount } from "@utils/hooks/useAccount";
import { arrayToNIP02 } from "@utils/transform";
import { useContext, useState } from "react";
import { useNavigate } from "react-router-dom";
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
import { updateAccount } from '@libs/storage';
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 = [
{
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 { account } = useAccount();
const { status, data } = useQuery(["trending-profiles"], async () => {
const res = await fetch("https://api.nostr.band/v0/trending/profiles");
const { status, data } = useQuery(['trending-profiles'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) {
throw new Error("Error");
throw new Error('Error');
}
return res.json();
});
@@ -135,10 +139,10 @@ export function CreateStep4Screen() {
const update = useMutation({
mutationFn: (follows: any) => {
return updateAccount("follows", follows, account.pubkey);
return updateAccount('follows', follows, account.pubkey);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
},
});
@@ -153,7 +157,7 @@ export function CreateStep4Screen() {
const event = new NDKEvent(ndk);
// build event
event.content = "";
event.content = '';
event.kind = 3;
event.pubkey = account.pubkey;
event.tags = tags;
@@ -164,9 +168,9 @@ export function CreateStep4Screen() {
update.mutate([...follows, account.pubkey]);
// redirect to next step
setTimeout(() => navigate("/auth/onboarding", { replace: true }), 1200);
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
} catch {
console.log("error");
console.log('error');
}
};
@@ -180,40 +184,35 @@ export function CreateStep4Screen() {
</h1>
</div>
<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">
Follow at least
<span className="text-fuchsia-500 font-semibold">
<span className="font-semibold text-fuchsia-500">
{follows.length}/10
</span>{" "}
</span>{' '}
plebs
</div>
{status === "loading" ? (
<div className="py-2 px-4 w-full h-11 inline-flex items-center justify-center">
{status === 'loading' ? (
<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" />
</div>
) : (
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
{list.map(
(item: { pubkey: string; profile: { content: string } }) => (
{list.map((item: { pubkey: string; profile: { content: string } }) => (
<button
key={item.pubkey}
type="button"
onClick={() => toggleFollow(item.pubkey)}
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
>
<User
pubkey={item.pubkey}
fallback={item.profile?.content}
/>
<User pubkey={item.pubkey} fallback={item.profile?.content} />
{follows.includes(item.pubkey) && (
<div>
<CheckCircleIcon className="w-4 h-4 text-green-400" />
<CheckCircleIcon className="h-4 w-4 text-green-400" />
</div>
)}
</button>
),
)}
))}
</div>
)}
</div>
@@ -221,12 +220,12 @@ export function CreateStep4Screen() {
<button
type="button"
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 ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
"Finish →"
'Finish →'
)}
</button>
)}

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,13 @@
import { usePublish } from "@libs/ndk";
import { LoaderIcon } from "@shared/icons";
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
import { User } from "@shared/user";
import { useAccount } from "@utils/hooks/useAccount";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { usePublish } from '@libs/ndk';
import { LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { User } from '@shared/user';
import { useAccount } from '@utils/hooks/useAccount';
export function OnboardingScreen() {
const publish = usePublish();
@@ -20,13 +23,13 @@ export function OnboardingScreen() {
// publish event
publish({
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,
tags: [],
});
// redirect to home
setTimeout(() => navigate("/", { replace: true }), 1200);
setTimeout(() => navigate('/', { replace: true }), 1200);
} catch (error) {
console.log(error);
}
@@ -40,27 +43,24 @@ export function OnboardingScreen() {
👋 Hello, welcome you to Lume
</h1>
<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 className="text-sm text-zinc-300">
If Lume gets your attention, please help us spread via button below
</p>
</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">
{status === "success" && (
<User
pubkey={account.pubkey}
time={Math.floor(Date.now() / 1000)}
/>
{status === 'success' && (
<User 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>
join us here:{" "}
join us here:{' '}
<a
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"
rel="noreferrer"
>
@@ -70,11 +70,11 @@ export function OnboardingScreen() {
</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
type="button"
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 ? (
<>
@@ -86,7 +86,7 @@ export function OnboardingScreen() {
<>
<span className="w-5" />
<span>Publish</span>
<ArrowRightCircleIcon className="w-5 h-5" />
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</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() {
return (
<div className="w-full h-full grid 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="w-full h-full flex flex-col justify-center px-4 py-4 gap-2">
<h1 className="text-zinc-700 text-4xl font-bold leading-none text-transparent">
<div className="grid h-full w-full grid-cols-12 gap-4 px-4 py-4">
<div className="col-span-5 flex flex-col rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex h-full w-full flex-col justify-center gap-2 px-4 py-4">
<h1 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Preserve your <span className="text-fuchsia-300">freedom</span>
</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>
</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>
</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>
</h3>
</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
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>Login with private key</span>
<ArrowRightCircleIcon className="w-5 h-5" />
<ArrowRightCircleIcon className="h-5 w-5" />
</Link>
<Link
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
</Link>
</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={{
backgroundImage: `url("https://void.cat/d/Ps1b36vu5pdkEA2w75usuB")`,
}}
/>
<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={{
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 { MuteIcon } from "@shared/icons";
import { Fragment } from "react";
import { Popover, Transition } from '@headlessui/react';
import { Fragment } from 'react';
import { MutedItem } from '@app/channel/components/mutedItem';
import { MuteIcon } from '@shared/icons';
export function ChannelBlackList({ blacklist }: { blacklist: any }) {
return (
@@ -10,9 +12,7 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
<>
<Popover.Button
className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 focus:outline-none ${
open
? "bg-zinc-800 hover:bg-zinc-700"
: "bg-zinc-900 hover:bg-zinc-800"
open ? 'bg-zinc-800 hover:bg-zinc-700' : 'bg-zinc-900 hover:bg-zinc-800'
}`}
>
<MuteIcon
@@ -31,15 +31,15 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
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">
<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="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">
Your muted list
</h3>
<p className="text-base leading-tight text-zinc-400">
Currently, unmute only affect locally, when you move to
new client, muted list will loaded again
Currently, unmute only affect locally, when you move to new client,
muted list will loaded again
</p>
</div>
</div>

View File

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

View File

@@ -1,6 +1,7 @@
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
import { NavLink } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { NavLink } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile';
export function ChannelsListItem({ data }: { data: any }) {
const channel = useChannelProfile(data.event_id);
@@ -10,19 +11,19 @@ export function ChannelsListItem({ data }: { data: any }) {
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
isActive ? "bg-zinc-900/50 text-zinc-100" : "",
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
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>
</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>
<div className="flex items-center">
{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}
</span>
)}

View File

@@ -1,7 +1,9 @@
import { ChannelCreateModal } from "@app/channel/components/createModal";
import { ChannelsListItem } from "@app/channel/components/item";
import { getChannels } from "@libs/storage";
import { useQuery } from "@tanstack/react-query";
import { useQuery } from '@tanstack/react-query';
import { ChannelCreateModal } from '@app/channel/components/createModal';
import { ChannelsListItem } from '@app/channel/components/item';
import { getChannels } from '@libs/storage';
export function ChannelsList() {
const {
@@ -9,7 +11,7 @@ export function ChannelsList() {
data: channels,
isFetching,
} = useQuery(
["channels"],
['channels'],
async () => {
return await getChannels();
},
@@ -17,12 +19,12 @@ export function ChannelsList() {
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
}
);
return (
<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="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 { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
export function Member({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey);

View File

@@ -1,22 +1,21 @@
import { Member } from "@app/channel/components/member";
import { getChannelUsers } from "@libs/storage";
import { useQuery } from "@tanstack/react-query";
import { useQuery } from '@tanstack/react-query';
import { Member } from '@app/channel/components/member';
import { getChannelUsers } from '@libs/storage';
export function ChannelMembers({ id }: { id: string }) {
const { status, data, isFetching } = useQuery(
["channel-members", id],
async () => {
const { status, data, isFetching } = useQuery(['channel-members', id], async () => {
return await getChannelUsers(id);
},
);
});
return (
<div className="mt-3">
<h5 className="border-b border-zinc-900 pb-1 font-semibold text-zinc-200">
Members
</h5>
<div className="mt-3 w-full flex flex-wrap gap-1.5">
{status === "loading" || isFetching ? (
<div className="mt-3 flex w-full flex-wrap gap-1.5">
{status === 'loading' || isFetching ? (
<p>Loading...</p>
) : (
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 { CancelIcon, EnterIcon } from "@shared/icons";
import { MediaUploader } from "@shared/mediaUploader";
import { RelayContext } from "@shared/relayProvider";
import { useChannelMessages } from "@stores/channels";
import { dateToUnix } from "@utils/date";
import { useAccount } from "@utils/hooks/useAccount";
import { useContext, useState } from "react";
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { useContext, useState } from 'react';
import { UserReply } from '@app/channel/components/messages/userReply';
import { CancelIcon, EnterIcon } from '@shared/icons';
import { MediaUploader } from '@shared/mediaUploader';
import { RelayContext } from '@shared/relayProvider';
import { useChannelMessages } from '@stores/channels';
import { dateToUnix } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
export function ChannelMessageForm({ channelID }: { channelID: string }) {
const ndk = useContext(RelayContext);
const [value, setValue] = useState("");
const [value, setValue] = useState('');
const [replyTo, closeReply] = useChannelMessages((state: any) => [
state.replyTo,
state.closeReply,
@@ -24,12 +28,12 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
if (replyTo.id !== null) {
tags = [
["e", channelID, "", "root"],
["e", replyTo.id, "", "reply"],
["p", replyTo.pubkey, ""],
['e', channelID, '', 'root'],
['e', replyTo.id, '', 'reply'],
['p', replyTo.pubkey, ''],
];
} else {
tags = [["e", channelID, "", "root"]];
tags = [['e', channelID, '', 'root']];
}
const signer = new NDKPrivateKeySigner(account.privkey);
@@ -47,11 +51,11 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
event.publish();
// reset state
setValue("");
setValue('');
};
const handleEnterPress = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submit();
}
@@ -62,7 +66,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
};
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 && (
<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">
@@ -89,11 +93,11 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
spellCheck={false}
placeholder="Message"
className={`relative ${
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`}
replyTo.id ? 'h-36 pt-16' : 'h-24 pt-3'
} 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="h-full flex gap-3 items-center justify-end text-zinc-500">
<div className="absolute bottom-0 right-2 h-11">
<div className="flex h-full items-center justify-end gap-3 text-zinc-500">
<MediaUploader setState={setValue} />
<button
type="button"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,17 @@
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import { NavLink } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { NavLink } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function ChatsListItem({ data }: { data: any }) {
const { status, user } = useProfile(data.sender_pubkey);
if (status === "loading") {
if (status === 'loading') {
return (
<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" />
@@ -23,12 +26,12 @@ export function ChatsListItem({ data }: { data: any }) {
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
isActive ? "bg-zinc-900/50 text-zinc-100" : "",
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
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
src={user?.image}
fallback={DEFAULT_AVATAR}
@@ -36,7 +39,7 @@ export function ChatsListItem({ data }: { data: any }) {
className="h-6 w-6 rounded object-cover"
/>
</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">
<h5 className="max-w-[10rem] truncate font-medium text-zinc-200">
{user?.nip05 ||
@@ -47,7 +50,7 @@ export function ChatsListItem({ data }: { data: any }) {
</div>
<div className="flex items-center">
{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}
</span>
)}

View File

@@ -1,9 +1,12 @@
import { ChatsListItem } from "@app/chat/components/item";
import { NewMessageModal } from "@app/chat/components/modal";
import { ChatsListSelfItem } from "@app/chat/components/self";
import { getChatsByPubkey } from "@libs/storage";
import { useQuery } from "@tanstack/react-query";
import { useAccount } from "@utils/hooks/useAccount";
import { useQuery } from '@tanstack/react-query';
import { ChatsListItem } from '@app/chat/components/item';
import { NewMessageModal } from '@app/chat/components/modal';
import { ChatsListSelfItem } from '@app/chat/components/self';
import { getChatsByPubkey } from '@libs/storage';
import { useAccount } from '@utils/hooks/useAccount';
export function ChatsList() {
const { account } = useAccount();
@@ -13,29 +16,29 @@ export function ChatsList() {
data: chats,
isFetching,
} = useQuery(
["chats"],
['chats'],
async () => {
const chats = await getChatsByPubkey(account.pubkey);
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;
},
{
enabled: account ? true : false,
},
}
);
if (status === "loading") {
if (status === 'loading') {
return (
<div className="flex flex-col">
<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="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 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="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>
);
@@ -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="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>
)}
{chats.map((item) => {
@@ -60,7 +63,7 @@ export function ChatsList() {
{isFetching && (
<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="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>

View File

@@ -1,15 +1,21 @@
import { usePublish } from "@libs/ndk";
import { EnterIcon } from "@shared/icons";
import { MediaUploader } from "@shared/mediaUploader";
import { nip04 } from "nostr-tools";
import { useCallback, useState } from "react";
import { nip04 } from 'nostr-tools';
import { useCallback, useState } from 'react';
import { usePublish } from '@libs/ndk';
import { EnterIcon } from '@shared/icons';
import { MediaUploader } from '@shared/mediaUploader';
export function ChatMessageForm({
receiverPubkey,
userPrivkey,
}: { receiverPubkey: string; userPubkey: string; userPrivkey: string }) {
}: {
receiverPubkey: string;
userPubkey: string;
userPrivkey: string;
}) {
const publish = usePublish();
const [value, setValue] = useState("");
const [value, setValue] = useState('');
const encryptMessage = useCallback(async () => {
return await nip04.encrypt(userPrivkey, receiverPubkey, value);
@@ -17,13 +23,13 @@ export function ChatMessageForm({
const submit = async () => {
const message = await encryptMessage();
const tags = [["p", receiverPubkey]];
const tags = [['p', receiverPubkey]];
// publish message
await publish({ content: message, kind: 4, tags });
// reset state
setValue("");
setValue('');
};
const handleEnterPress = (e: {
@@ -31,7 +37,7 @@ export function ChatMessageForm({
shiftKey: any;
preventDefault: () => void;
}) => {
if (e.key === "Enter" && !e.shiftKey) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submit();
}
@@ -45,10 +51,10 @@ export function ChatMessageForm({
onKeyDown={handleEnterPress}
spellCheck={false}
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="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} />
<button
type="button"

View File

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

View File

@@ -1,9 +1,12 @@
import { User } from "@app/auth/components/user";
import { Dialog, Transition } from "@headlessui/react";
import { CancelIcon, LoaderIcon, PlusIcon } from "@shared/icons";
import { useAccount } from "@utils/hooks/useAccount";
import { Fragment, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
import { CancelIcon, LoaderIcon, PlusIcon } from '@shared/icons';
import { useAccount } from '@utils/hooks/useAccount';
export function NewMessageModal() {
const navigate = useNavigate();
@@ -77,22 +80,17 @@ export function NewMessageModal() {
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon
width={20}
height={20}
className="text-zinc-300"
/>
<CancelIcon width={20} height={20} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
All messages will be encrypted, but anyone can see who you
chat
All messages will be encrypted, but anyone can see who you chat
</Dialog.Description>
</div>
</div>
<div className="h-[500px] flex flex-col pb-5 overflow-x-hidden overflow-y-auto">
{status === "loading" ? (
<div className="px-4 py-3 inline-flex items-center justify-center">
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-5">
{status === 'loading' ? (
<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" />
</div>
) : (
@@ -106,7 +104,7 @@ export function NewMessageModal() {
<button
type="button"
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
</button>

View File

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

View File

@@ -1,8 +1,11 @@
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import { Link } from "react-router-dom";
import { Link } from 'react-router-dom';
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function ChatSidebar({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey);
@@ -20,7 +23,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
</div>
<div className="flex flex-col gap-4">
<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}
</h3>
<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>
<Link
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
</Link>

View File

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

View File

@@ -1,14 +1,18 @@
import { ChatMessageForm } from "@app/chat/components/messages/form";
import { ChatMessageItem } from "@app/chat/components/messages/item";
import { ChatSidebar } from "@app/chat/components/sidebar";
import { createChat, getChatMessages } from "@libs/storage";
import { NDKSubscription } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAccount } from "@utils/hooks/useAccount";
import { useCallback, useContext, useEffect, useRef } from "react";
import { useParams } from "react-router-dom";
import { Virtuoso } from "react-virtuoso";
import { NDKSubscription } from '@nostr-dev-kit/ndk';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useContext, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { Virtuoso } from 'react-virtuoso';
import { ChatMessageForm } from '@app/chat/components/messages/form';
import { ChatMessageItem } from '@app/chat/components/messages/item';
import { ChatSidebar } from '@app/chat/components/sidebar';
import { createChat, getChatMessages } from '@libs/storage';
import { RelayContext } from '@shared/relayProvider';
import { useAccount } from '@utils/hooks/useAccount';
export function ChatScreen() {
const ndk = useContext(RelayContext);
@@ -18,13 +22,13 @@ export function ChatScreen() {
const { pubkey } = useParams();
const { account } = useAccount();
const { status, data } = useQuery(
["chat", pubkey],
['chat', pubkey],
async () => {
return await getChatMessages(account.pubkey, pubkey);
},
{
enabled: account ? true : false,
},
}
);
const itemContent: any = useCallback(
@@ -37,14 +41,14 @@ export function ChatScreen() {
/>
);
},
[data],
[data]
);
const computeItemKey = useCallback(
(index: string | number) => {
return data[index].id;
},
[data],
[data]
);
const chat = useMutation({
@@ -55,11 +59,11 @@ export function ChatScreen() {
data.sender_pubkey,
data.content,
data.tags,
data.created_at,
data.created_at
);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["chat", pubkey] });
queryClient.invalidateQueries({ queryKey: ['chat', pubkey] });
},
});
@@ -68,15 +72,15 @@ export function ChatScreen() {
{
kinds: [4],
authors: [account.pubkey],
"#p": [pubkey],
'#p': [pubkey],
since: Math.floor(Date.now() / 1000),
},
{
closeOnEose: false,
},
}
);
sub.addListener("event", (event) => {
sub.addListener('event', (event) => {
chat.mutate({
id: event.id,
receiver_pubkey: pubkey,
@@ -93,18 +97,18 @@ export function ChatScreen() {
}, [pubkey]);
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
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>
</div>
<div className="w-full h-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-1 w-full h-full">
{status === "loading" ? (
<div className="h-full w-full flex-1 p-3">
<div className="flex h-full flex-col justify-between overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="h-full w-full flex-1">
{status === 'loading' ? (
<p>Loading...</p>
) : (
<Virtuoso
@@ -117,14 +121,14 @@ export function ChatScreen() {
followOutput={true}
overscan={50}
increaseViewportBy={{ top: 200, bottom: 200 }}
className="relative scrollbar-hide overflow-y-auto"
className="scrollbar-hide relative overflow-y-auto"
components={{
EmptyPlaceholder: () => Empty,
}}
/>
)}
</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
receiverPubkey={pubkey}
userPubkey={account.pubkey}
@@ -137,7 +141,7 @@ export function ChatScreen() {
<div className="col-span-1">
<div
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} />
</div>
@@ -146,10 +150,10 @@ export function ChatScreen() {
}
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>
<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>
</div>
);

View File

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

View File

@@ -1,50 +1,55 @@
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
import { useAccount } from "@utils/hooks/useAccount";
import { useState } from "react";
import { useState } from 'react';
import { EyeOffIcon, EyeOnIcon } from '@shared/icons';
import { useAccount } from '@utils/hooks/useAccount';
export function AccountSettingsScreen() {
const { status, account } = useAccount();
const [type, setType] = useState("password");
const [type, setType] = useState('password');
const showPrivateKey = () => {
if (type === "password") {
setType("text");
if (type === 'password') {
setType('text');
} else {
setType("password");
setType('password');
}
};
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">
<h1 className="text-lg font-semibold text-zinc-100">Account</h1>
<div className="">
{status === "loading" ? (
{status === 'loading' ? (
<p>Loading...</p>
) : (
<div className="flex flex-col gap-4">
<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
</label>
<input
readOnly
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 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
</label>
<input
readOnly
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 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
</label>
<div className="relative w-2/3">
@@ -52,14 +57,14 @@ export function AccountSettingsScreen() {
readOnly
type={type}
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
type="button"
onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{type === "password" ? (
{type === 'password' ? (
<EyeOffIcon
width={20}
height={20}

View File

@@ -1,8 +1,9 @@
import { Switch } from "@headlessui/react";
import { getSetting, updateSetting } from "@libs/storage";
import { useEffect, useState } from "react";
import { twMerge } from "tailwind-merge";
import { disable, enable, isEnabled } from "tauri-plugin-autostart-api";
import { Switch } from '@headlessui/react';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import { disable, enable, isEnabled } from 'tauri-plugin-autostart-api';
import { getSetting, updateSetting } from '@libs/storage';
export function AutoStartSetting() {
const [enabled, setEnabled] = useState(false);
@@ -10,18 +11,18 @@ export function AutoStartSetting() {
const toggle = async () => {
if (!enabled) {
await enable();
await updateSetting("auto_start", 1);
await updateSetting('auto_start', 1);
console.log(`registered for autostart? ${await isEnabled()}`);
} else {
await disable();
await updateSetting("auto_start", 0);
await updateSetting('auto_start', 0);
}
setEnabled(!enabled);
};
useEffect(() => {
async function getAppSetting() {
const setting = await getSetting("auto_start");
const setting = await getSetting('auto_start');
if (parseInt(setting) === 0) {
setEnabled(false);
} else {
@@ -32,27 +33,23 @@ export function AutoStartSetting() {
}, []);
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">
<span className="leading-none font-medium text-zinc-200">
Auto start
</span>
<span className="leading-none text-sm text-zinc-400">
Auto start at login
</span>
<span className="font-medium leading-none text-zinc-200">Auto start</span>
<span className="text-sm leading-none text-zinc-400">Auto start at login</span>
</div>
<Switch
checked={enabled}
onChange={toggle}
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",
enabled ? "bg-fuchsia-500" : "bg-zinc-700",
'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'
)}
>
<span
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",
enabled ? "translate-x-5" : "translate-x-0",
'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'
)}
/>
</Switch>

View File

@@ -1,24 +1,24 @@
import { getSetting, updateSetting } from "@libs/storage";
import { CheckCircleIcon } from "@shared/icons";
import { useState } from "react";
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;
export function CacheTimeSetting() {
const [time, setTime] = useState(cacheTime);
const update = async () => {
await updateSetting("cache_time", time);
await updateSetting('cache_time', time);
};
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">
<span className="leading-none font-medium text-zinc-200">
Cache time
</span>
<span className="leading-none text-sm text-zinc-400">
<span className="font-medium leading-none text-zinc-200">Cache time</span>
<span className="text-sm leading-none text-zinc-400">
The length of time before inactive data gets removed from the cache
</span>
</div>
@@ -28,14 +28,14 @@ export function CacheTimeSetting() {
onChange={(e) => setTime(e.currentTarget.value)}
autoCapitalize="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
type="button"
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>
</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();
export function VersionSetting() {
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">
<span className="leading-none font-medium text-zinc-200">Version</span>
<span className="leading-none text-sm text-zinc-400">
You're using latest version
<span className="font-medium leading-none text-zinc-200">Version</span>
<span className="text-sm leading-none text-zinc-400">
You&apos;re using latest version
</span>
</div>
<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
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>
</div>
</div>

View File

@@ -1,14 +1,14 @@
import { AutoStartSetting } from "@app/settings/components/autoStart";
import { CacheTimeSetting } from "@app/settings/components/cacheTime";
import { VersionSetting } from "@app/settings/components/version";
import { AutoStartSetting } from '@app/settings/components/autoStart';
import { CacheTimeSetting } from '@app/settings/components/cacheTime';
import { VersionSetting } from '@app/settings/components/version';
export function GeneralSettingsScreen() {
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">
<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 h-full flex flex-col divide-y divide-zinc-800">
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex h-full w-full flex-col divide-y divide-zinc-800">
<AutoStartSetting />
<CacheTimeSetting />
<VersionSetting />

View File

@@ -1,104 +1,84 @@
import { CommandIcon } from "@shared/icons";
import { CommandIcon } from '@shared/icons';
export function ShortcutsSettingsScreen() {
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">
<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 h-full flex flex-col divide-y divide-zinc-800">
<div className="px-5 py-4 inline-flex items-center justify-between">
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex h-full w-full flex-col divide-y divide-zinc-800">
<div className="inline-flex items-center justify-between px-5 py-4">
<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
</span>
</div>
<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">
<CommandIcon
width={12}
height={12}
className="text-zinc-500"
/>
<CommandIcon width={12} height={12} className="text-zinc-500" />
</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">
<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 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">
<span className="leading-none font-medium text-zinc-200">
<span className="font-medium leading-none text-zinc-200">
Add image block
</span>
</div>
<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">
<CommandIcon
width={12}
height={12}
className="text-zinc-500"
/>
<CommandIcon width={12} height={12} className="text-zinc-500" />
</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">
<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 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">
<span className="leading-none font-medium text-zinc-200">
<span className="font-medium leading-none text-zinc-200">
Add newsfeed block
</span>
</div>
<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">
<CommandIcon
width={12}
height={12}
className="text-zinc-500"
/>
<CommandIcon width={12} height={12} className="text-zinc-500" />
</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">
<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 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">
<span className="leading-none font-medium text-zinc-200">
<span className="font-medium leading-none text-zinc-200">
Open personal page
</span>
</div>
<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">
<CommandIcon
width={12}
height={12}
className="text-zinc-500"
/>
<CommandIcon width={12} height={12} className="text-zinc-500" />
</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">
<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 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">
<span className="leading-none font-medium text-zinc-200">
<span className="font-medium leading-none text-zinc-200">
Open notification
</span>
</div>
<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">
<CommandIcon
width={12}
height={12}
className="text-zinc-500"
/>
<CommandIcon width={12} height={12} className="text-zinc-500" />
</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">
<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>

View File

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

View File

@@ -1,16 +1,21 @@
import { User } from "@app/auth/components/user";
import { Dialog, Transition } from "@headlessui/react";
import { Combobox } from "@headlessui/react";
import { createBlock } from "@libs/storage";
import { CancelIcon, CheckCircleIcon, CommandIcon } from "@shared/icons";
import { DEFAULT_AVATAR } from "@stores/constants";
import { ADD_FEEDBLOCK_SHORTCUT } from "@stores/shortcuts";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAccount } from "@utils/hooks/useAccount";
import { nip19 } from "nostr-tools";
import { Fragment, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useHotkeys } from "react-hotkeys-hook";
import { Dialog, Transition } from '@headlessui/react';
import { Combobox } from '@headlessui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { Fragment, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { User } from '@app/auth/components/user';
import { createBlock } from '@libs/storage';
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() {
const queryClient = useQueryClient();
@@ -18,7 +23,7 @@ export function AddFeedBlock() {
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState([]);
const [query, setQuery] = useState("");
const [query, setQuery] = useState('');
const { status, account } = useAccount();
@@ -37,7 +42,7 @@ export function AddFeedBlock() {
return createBlock(data.kind, data.title, data.content);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["blocks"] });
queryClient.invalidateQueries({ queryKey: ['blocks'] });
},
});
@@ -52,7 +57,7 @@ export function AddFeedBlock() {
setLoading(true);
selected.forEach((item, index) => {
if (item.substring(0, 4) === "npub") {
if (item.substring(0, 4) === 'npub') {
selected[index] = nip19.decode(item).data;
}
});
@@ -76,14 +81,14 @@ export function AddFeedBlock() {
<button
type="button"
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="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" />
</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">
<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>
@@ -128,60 +133,53 @@ export function AddFeedBlock() {
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon
width={14}
height={14}
className="text-zinc-300"
/>
<CancelIcon width={14} height={14} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
Specific newsfeed space for people you want to keep up to
date
Specific newsfeed space for people you want to keep up to date
</Dialog.Description>
</div>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0"
className="mb-0 flex h-full w-full flex-col gap-4"
>
<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 *
</label>
<input
type={"text"}
{...register("title", {
type={'text'}
{...register('title', {
required: true,
})}
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 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 *
</label>
<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">
</span>
<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">
<Combobox
value={selected}
onChange={setSelected}
multiple
>
<Combobox value={selected} onChange={setSelected} multiple>
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
spellCheck={false}
autoFocus={false}
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>
{query.length > 0 && (
<Combobox.Option
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 }) => (
<>
@@ -189,7 +187,7 @@ export function AddFeedBlock() {
<img
alt={query}
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">
<span className="text-base leading-tight text-zinc-400">
@@ -198,26 +196,26 @@ export function AddFeedBlock() {
</div>
</div>
{selected && (
<CheckCircleIcon className="w-4 h-4 text-green-500" />
<CheckCircleIcon className="h-4 w-4 text-green-500" />
)}
</>
)}
</Combobox.Option>
)}
{status === "loading" ? (
{status === 'loading' ? (
<p>Loading...</p>
) : (
JSON.parse(account.follows).map((follow) => (
<Combobox.Option
key={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 }) => (
<>
<User pubkey={follow} />
{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
type="submit"
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 ? (
<svg
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>
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
"Confirm"
'Confirm'
)}
</button>
</div>

View File

@@ -1,20 +1,24 @@
import { Dialog, Transition } from "@headlessui/react";
import { createBlock } from "@libs/storage";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CancelIcon, CommandIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import { DEFAULT_AVATAR } from "@stores/constants";
import { ADD_IMAGEBLOCK_SHORTCUT } from "@stores/shortcuts";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { open } from "@tauri-apps/api/dialog";
import { Body, fetch } from "@tauri-apps/api/http";
import { createBlobFromFile } from "@utils/createBlobFromFile";
import { dateToUnix } from "@utils/date";
import { useAccount } from "@utils/hooks/useAccount";
import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useHotkeys } from "react-hotkeys-hook";
import { Dialog, Transition } from '@headlessui/react';
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { open } from '@tauri-apps/api/dialog';
import { Body, fetch } from '@tauri-apps/api/http';
import { Fragment, useContext, useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { createBlock } from '@libs/storage';
import { CancelIcon, CommandIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { RelayContext } from '@shared/relayProvider';
import { DEFAULT_AVATAR } from '@stores/constants';
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() {
const ndk = useContext(RelayContext);
@@ -22,7 +26,7 @@ export function AddImageBlock() {
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState("");
const [image, setImage] = useState('');
const { account } = useAccount();
@@ -51,8 +55,8 @@ export function AddImageBlock() {
multiple: false,
filters: [
{
name: "Image",
extensions: ["png", "jpeg", "jpg"],
name: 'Image',
extensions: ['png', 'jpeg', 'jpg'],
},
],
});
@@ -62,19 +66,19 @@ export function AddImageBlock() {
} else if (selected === null) {
// user cancelled the selection
} else {
const filename = selected.split("/").pop();
const filename = selected.split('/').pop();
const file = await createBlobFromFile(selected);
const buf = await file.arrayBuffer();
const res: any = await fetch("https://void.cat/upload?cli=false", {
method: "POST",
const res: any = await fetch('https://void.cat/upload?cli=false', {
method: 'POST',
timeout: 5,
headers: {
accept: "*/*",
"Content-Type": "application/octet-stream",
"V-Filename": filename,
"V-Description": "Upload from https://lume.nu",
"V-Strip-Metadata": "true",
accept: '*/*',
'Content-Type': 'application/octet-stream',
'V-Filename': filename,
'V-Description': 'Upload from https://lume.nu',
'V-Strip-Metadata': 'true',
},
body: Body.bytes(buf),
});
@@ -82,11 +86,11 @@ export function AddImageBlock() {
if (res.ok) {
const imageURL = `https://void.cat/d/${res.data.file.id}.webp`;
tags.current = [
["url", imageURL],
["m", res.data.file.metadata.mimeType],
["x", res.data.file.metadata.digest],
["size", res.data.file.metadata.size],
["magnet", res.data.file.metadata.magnetLink],
['url', imageURL],
['m', res.data.file.metadata.mimeType],
['x', res.data.file.metadata.digest],
['size', res.data.file.metadata.size],
['magnet', res.data.file.metadata.magnetLink],
];
setImage(imageURL);
@@ -99,7 +103,7 @@ export function AddImageBlock() {
return createBlock(data.kind, data.title, data.content);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["blocks"] });
queryClient.invalidateQueries({ queryKey: ['blocks'] });
},
});
@@ -131,7 +135,7 @@ export function AddImageBlock() {
};
useEffect(() => {
setValue("content", image);
setValue('content', image);
}, [setValue, image]);
return (
@@ -139,14 +143,14 @@ export function AddImageBlock() {
<button
type="button"
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="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" />
</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">
<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>
@@ -191,48 +195,49 @@ export function AddImageBlock() {
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon
width={14}
height={14}
className="text-zinc-300"
/>
<CancelIcon width={14} height={14} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
Pin your favorite image to Space then you can view every
time that you use Lume, your image will be broadcast to
Nostr Relay as well
Pin your favorite image to Space then you can view every time that
you use Lume, your image will be broadcast to Nostr Relay as well
</Dialog.Description>
</div>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0"
className="mb-0 flex h-full w-full flex-col gap-4"
>
<input
type={"hidden"}
{...register("content")}
type={'hidden'}
{...register('content')}
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">
<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 *
</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
type={"text"}
{...register("title", {
type={'text'}
{...register('title', {
required: true,
})}
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 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
</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">
@@ -240,7 +245,7 @@ export function AddImageBlock() {
src={image}
fallback={DEFAULT_AVATAR}
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">
<button
@@ -257,7 +262,7 @@ export function AddImageBlock() {
<button
type="submit"
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 ? (
<svg
@@ -282,7 +287,7 @@ export function AddImageBlock() {
/>
</svg>
) : (
"Confirm"
'Confirm'
)}
</button>
</div>

View File

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

View File

@@ -1,12 +1,16 @@
import { useNewsfeed } from "@app/space/hooks/useNewsfeed";
import { getNotes } from "@libs/storage";
import { Note } from "@shared/notes/note";
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar";
import { useNote } from "@stores/note";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useEffect, useRef } from "react";
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useEffect, useRef } from 'react';
import { useNewsfeed } from '@app/space/hooks/useNewsfeed';
import { getNotes } from '@libs/storage';
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;
@@ -18,15 +22,9 @@ export function FollowingBlock({ block }: { block: number }) {
state.toggleHasNewNote,
]);
const {
status,
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
}: any = useInfiniteQuery({
queryKey: ["newsfeed-circle"],
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch }: any =
useInfiniteQuery({
queryKey: ['newsfeed-circle'],
queryFn: async ({ pageParam = 0 }) => {
return await getNotes(ITEM_PER_PAGE, pageParam);
},
@@ -52,11 +50,7 @@ export function FollowingBlock({ block }: { block: number }) {
return;
}
if (
lastItem.index >= notes.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
if (lastItem.index >= notes.length - 1 && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
@@ -81,14 +75,14 @@ export function FollowingBlock({ block }: { block: number }) {
};
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" />
{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
type="button"
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
</button>
@@ -96,10 +90,10 @@ export function FollowingBlock({ block }: { block: number }) {
)}
<div
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"
style={{ contain: "strict" }}
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' }}
>
{status === "loading" ? (
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<NoteSkeleton />
@@ -116,8 +110,7 @@ export function FollowingBlock({ block }: { block: number }) {
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${
itemsVirtualizer[0].start -
rowVirtualizer.options.scrollMargin
itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
}px)`,
}}
>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,14 @@
import { AddBlock } from "@app/space/components/add";
import { FeedBlock } from "@app/space/components/blocks/feed";
import { FollowingBlock } from "@app/space/components/blocks/following";
import { ImageBlock } from "@app/space/components/blocks/image";
import { ThreadBlock } from "@app/space/components/blocks/thread";
import { getBlocks } from "@libs/storage";
import { LoaderIcon } from "@shared/icons";
import { useQuery } from "@tanstack/react-query";
import { useQuery } from '@tanstack/react-query';
import { AddBlock } from '@app/space/components/add';
import { FeedBlock } from '@app/space/components/blocks/feed';
import { FollowingBlock } from '@app/space/components/blocks/following';
import { ImageBlock } from '@app/space/components/blocks/image';
import { ThreadBlock } from '@app/space/components/blocks/thread';
import { getBlocks } from '@libs/storage';
import { LoaderIcon } from '@shared/icons';
export function SpaceScreen() {
const {
@@ -13,7 +16,7 @@ export function SpaceScreen() {
data: blocks,
isFetching,
} = useQuery(
["blocks"],
['blocks'],
async () => {
return await getBlocks();
},
@@ -22,20 +25,20 @@ export function SpaceScreen() {
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
}
);
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} />
{status === "loading" ? (
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
{status === 'loading' ? (
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
<div
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" />
</div>
</div>
@@ -54,23 +57,23 @@ export function SpaceScreen() {
})
)}
{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
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" />
</div>
</div>
)}
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
<div className="w-full h-full inline-flex items-center justify-center">
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
<div className="inline-flex h-full w-full items-center justify-center">
<AddBlock />
</div>
</div>
<div className="shrink-0 w-[350px]" />
<div className="w-[350px] shrink-0" />
</div>
);
}

View File

@@ -1,22 +1,20 @@
import { FollowIcon, LoaderIcon, UnfollowIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useQuery } from "@tanstack/react-query";
import { useSocial } from "@utils/hooks/useSocial";
import { compactNumber } from "@utils/number";
import { shortenKey } from "@utils/shortenKey";
import { useEffect, useState } from "react";
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { FollowIcon, LoaderIcon, UnfollowIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useSocial } from '@utils/hooks/useSocial';
import { compactNumber } from '@utils/number';
import { shortenKey } from '@utils/shortenKey';
export function Profile({ data }: { data: any }) {
const { status, data: userStats } = useQuery(
["user-stats", data.pubkey],
async () => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${data.pubkey}`,
);
const { status, data: userStats } = useQuery(['user-stats', data.pubkey], async () => {
const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
return res.json();
},
);
});
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
const profile = embedProfile;
@@ -45,7 +43,7 @@ export function Profile({ data }: { data: any }) {
};
useEffect(() => {
if (status === "success" && userFollows) {
if (status === 'success' && userFollows) {
if (userFollows.includes(data.pubkey)) {
setFollowed(true);
}
@@ -55,7 +53,7 @@ export function Profile({ data }: { data: any }) {
if (!profile)
return (
<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>
);
@@ -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="flex items-center justify-between">
<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
src={profile.picture}
fallback={DEFAULT_AVATAR}
className="w-11 h-11 object-cover rounded-lg"
className="h-11 w-11 rounded-lg object-cover"
/>
</div>
<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}
</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)}
</p>
</div>
</div>
<div className="inline-flex items-center gap-2">
{socialStatus === "loading" ? (
{socialStatus === 'loading' ? (
<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" />
</button>
@@ -91,17 +89,17 @@ export function Profile({ data }: { data: any }) {
<button
type="button"
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
type="button"
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>
)}
</div>
@@ -112,37 +110,31 @@ export function Profile({ data }: { data: any }) {
</p>
</div>
<div className="mt-8">
{status === "loading" ? (
{status === 'loading' ? (
<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">
<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}
</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 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}
</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 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
? compactNumber.format(
userStats.stats[data.pubkey].zaps_received.msats / 1000,
userStats.stats[data.pubkey].zaps_received.msats / 1000
)
: 0}
</span>
<span className="leading-none text-sm text-zinc-400">
Zaps received
</span>
<span className="text-sm leading-none text-zinc-400">Zaps received</span>
</div>
</div>
)}

View File

@@ -1,30 +1,31 @@
import { Note } from "@shared/notes/note";
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar";
import { useQuery } from "@tanstack/react-query";
import { useQuery } from '@tanstack/react-query';
import { Note } from '@shared/notes/note';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
export function TrendingNotes() {
const { status, data, error } = useQuery(["trending-notes"], async () => {
const res = await fetch("https://api.nostr.band/v0/trending/notes");
const { status, data, error } = useQuery(['trending-notes'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/notes');
if (!res.ok) {
throw new Error("Error");
throw new Error('Error');
}
return res.json();
});
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" />
<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>}
{status === "loading" ? (
{status === 'loading' ? (
<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 />
</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) => (
<Note key={item.id} event={item.event} />
))}

View File

@@ -1,30 +1,32 @@
import { Profile } from "@app/trending/components/profile";
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar";
import { useQuery } from "@tanstack/react-query";
import { useQuery } from '@tanstack/react-query';
import { Profile } from '@app/trending/components/profile';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
export function TrendingProfiles() {
const { status, data, error } = useQuery(["trending-profiles"], async () => {
const res = await fetch("https://api.nostr.band/v0/trending/profiles");
const { status, data, error } = useQuery(['trending-profiles'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) {
throw new Error("Error");
throw new Error('Error');
}
return res.json();
});
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" />
<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>}
{status === "loading" ? (
{status === 'loading' ? (
<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 />
</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) => (
<Profile key={item.pubkey} data={item} />
))}

View File

@@ -1,9 +1,9 @@
import { TrendingNotes } from "@app/trending/components/trendingNotes";
import { TrendingProfiles } from "@app/trending/components/trendingProfiles";
import { TrendingNotes } from '@app/trending/components/trendingNotes';
import { TrendingProfiles } from '@app/trending/components/trendingProfiles';
export function TrendingScreen() {
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 />
<TrendingNotes />
</div>

View File

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

View File

@@ -1,54 +1,49 @@
import { useQuery } from "@tanstack/react-query";
import { compactNumber } from "@utils/number";
import { useQuery } from '@tanstack/react-query';
import { compactNumber } from '@utils/number';
export function UserMetadata({ pubkey }: { pubkey: string }) {
const { status, data } = useQuery(["user-metadata", pubkey], async () => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
);
const { status, data } = useQuery(['user-metadata', pubkey], async () => {
const res = await fetch(`https://api.nostr.band/v0/stats/profile/${pubkey}`);
if (!res.ok) {
throw new Error("Error");
throw new Error('Error');
}
return await res.json();
});
if (status === "loading") {
if (status === 'loading') {
return <p>Loading...</p>;
}
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">
<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}
</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 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}
</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 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
? compactNumber.format(
data.stats[pubkey].zaps_received.msats / 1000,
)
? compactNumber.format(data.stats[pubkey].zaps_received.msats / 1000)
: 0}
</span>
<span className="leading-none text-sm text-zinc-400">
Zaps received
</span>
<span className="text-sm leading-none text-zinc-400">Zaps received</span>
</div>
<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
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
: 0}
</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>
);

View File

@@ -1,16 +1,20 @@
import { UserFeed } from "@app/user/components/feed";
import { UserMetadata } from "@app/user/components/metadata";
import { Tab } from "@headlessui/react";
import { EditProfileModal } from "@shared/editProfileModal";
import { ThreadsIcon, ZapIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useAccount } from "@utils/hooks/useAccount";
import { useProfile } from "@utils/hooks/useProfile";
import { useSocial } from "@utils/hooks/useSocial";
import { shortenKey } from "@utils/shortenKey";
import { Fragment, useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { Tab } from '@headlessui/react';
import { Fragment, useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { UserFeed } from '@app/user/components/feed';
import { UserMetadata } from '@app/user/components/metadata';
import { EditProfileModal } from '@shared/editProfileModal';
import { ThreadsIcon, ZapIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
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() {
const { pubkey } = useParams();
@@ -43,7 +47,7 @@ export function UserScreen() {
};
useEffect(() => {
if (status === "success" && userFollows) {
if (status === 'success' && userFollows) {
if (userFollows.includes(pubkey)) {
setFollowed(true);
}
@@ -54,39 +58,39 @@ export function UserScreen() {
<div className="h-full w-full overflow-y-auto">
<div
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
src={user?.banner}
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
alt={"banner"}
className="w-full h-full object-cover"
alt={'banner'}
className="h-full w-full object-cover"
/>
</div>
<div className="w-full -mt-7">
<div className="-mt-7 w-full">
<div className="px-5">
<Image
src={user?.image}
fallback={DEFAULT_AVATAR}
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="inline-flex flex-col gap-1.5">
<h5 className="font-semibold text-lg leading-none">
{user?.displayName || user?.name || "No name"}
<h5 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name || 'No name'}
</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)}
</span>
</div>
<div className="inline-flex items-center gap-2">
{status === "loading" ? (
{status === 'loading' ? (
<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...
</button>
@@ -94,7 +98,7 @@ export function UserScreen() {
<button
type="button"
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
</button>
@@ -102,29 +106,29 @@ export function UserScreen() {
<button
type="button"
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
</button>
)}
<Link
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
</Link>
<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>
<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 />}
</div>
</div>
<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}
</p>
<UserMetadata pubkey={pubkey} />
@@ -133,18 +137,18 @@ export function UserScreen() {
</div>
<div className="mt-8 w-full border-t border-zinc-900">
<Tab.Group>
<Tab.List className="px-5 mb-2">
<Tab.List className="mb-2 px-5">
<Tab as={Fragment}>
{({ selected }) => (
<button
type="button"
className={`${
selected
? "text-fuchsia-500 border-fuchsia-500"
: "text-zinc-200 border-transparent"
} font-medium inline-flex items-center gap-2 h-10 border-t`}
? 'border-fuchsia-500 text-fuchsia-500'
: 'border-transparent text-zinc-200'
} 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
</button>
)}

View File

@@ -15,7 +15,7 @@ button {
}
.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) */

View File

@@ -4,11 +4,14 @@ import NDK, {
NDKFilter,
NDKKind,
NDKPrivateKeySigner,
} from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider";
import { FULL_RELAYS } from "@stores/constants";
import { useAccount } from "@utils/hooks/useAccount";
import { useContext } from "react";
} from '@nostr-dev-kit/ndk';
import { useContext } from 'react';
import { RelayContext } from '@shared/relayProvider';
import { FULL_RELAYS } from '@stores/constants';
import { useAccount } from '@utils/hooks/useAccount';
export async function initNDK(relays?: string[]): Promise<NDK> {
const opts: NDKConstructorParams = {};
@@ -24,7 +27,7 @@ export async function initNDK(relays?: string[]): Promise<NDK> {
export async function prefetchEvents(
ndk: NDK,
filter: NDKFilter,
filter: NDKFilter
): Promise<Set<NDKEvent>> {
return new Promise((resolve) => {
const events: Map<string, NDKEvent> = new Map();
@@ -33,12 +36,12 @@ export async function prefetchEvents(
closeOnEose: true,
});
relaySetSubscription.on("event", (event: NDKEvent) => {
relaySetSubscription.on('event', (event: NDKEvent) => {
event.ndk = ndk;
events.set(event.tagId(), event);
});
relaySetSubscription.on("eose", () => {
relaySetSubscription.on('eose', () => {
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 * as cheerio from "cheerio";
import { FetchOptions, ResponseType, fetch } from '@tauri-apps/api/http';
import * as cheerio from 'cheerio';
import { OPENGRAPH } from '@stores/constants';
interface ILinkPreviewOptions {
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) {
return doc(`meta[${attr}='${type}']`).attr("content");
return doc(`meta[${attr}='${type}']`).attr('content');
}
function getTitle(doc: cheerio.CheerioAPI) {
let title =
metaTagContent(doc, "og:title", "property") ||
metaTagContent(doc, "og:title", "name");
metaTagContent(doc, 'og:title', 'property') ||
metaTagContent(doc, 'og:title', 'name');
if (!title) {
title = doc("title").text();
title = doc('title').text();
}
return title;
}
function getSiteName(doc: cheerio.CheerioAPI) {
const siteName =
metaTagContent(doc, "og:site_name", "property") ||
metaTagContent(doc, "og:site_name", "name");
metaTagContent(doc, 'og:site_name', 'property') ||
metaTagContent(doc, 'og:site_name', 'name');
return siteName;
}
function getDescription(doc: cheerio.CheerioAPI) {
const description =
metaTagContent(doc, "description", "name") ||
metaTagContent(doc, "Description", "name") ||
metaTagContent(doc, "og:description", "property");
metaTagContent(doc, 'description', 'name') ||
metaTagContent(doc, 'Description', 'name') ||
metaTagContent(doc, 'og:description', 'property');
return description;
}
function getMediaType(doc: cheerio.CheerioAPI) {
const node = metaTag(doc, "medium", "name");
const node = metaTag(doc, 'medium', 'name');
if (node) {
const content = node.attr("content");
return content === "image" ? "photo" : content;
const content = node.attr('content');
return content === 'image' ? 'photo' : content;
}
return (
metaTagContent(doc, "og:type", "property") ||
metaTagContent(doc, "og:type", "name")
metaTagContent(doc, 'og:type', 'property') || metaTagContent(doc, 'og:type', 'name')
);
}
function getImages(
doc: cheerio.CheerioAPI,
rootUrl: string,
imagesPropertyType?: string,
imagesPropertyType?: string
) {
let images: string[] = [];
let nodes: cheerio.Cheerio<cheerio.Element> | null;
let src: string | undefined;
let dic: Record<string, boolean> = {};
const imagePropertyType = imagesPropertyType ?? "og";
const imagePropertyType = imagesPropertyType ?? 'og';
nodes =
metaTag(doc, `${imagePropertyType}:image`, "property") ||
metaTag(doc, `${imagePropertyType}:image`, "name");
metaTag(doc, `${imagePropertyType}:image`, 'property') ||
metaTag(doc, `${imagePropertyType}:image`, 'name');
if (nodes) {
nodes.each((_: number, node: cheerio.Element) => {
if (node.type === "tag") {
if (node.type === 'tag') {
src = node.attribs.content;
if (src) {
src = new URL(src, rootUrl).href;
@@ -95,18 +95,18 @@ function getImages(
}
if (images.length <= 0 && !imagesPropertyType) {
src = doc("link[rel=image_src]").attr("href");
src = doc('link[rel=image_src]').attr('href');
if (src) {
src = new URL(src, rootUrl).href;
images = [src];
} else {
nodes = doc("img");
nodes = doc('img');
if (nodes?.length) {
dic = {};
images = [];
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]) {
dic[src] = true;
// width = node.attribs.width;
@@ -135,34 +135,32 @@ function getVideos(doc: cheerio.CheerioAPI) {
let videoObj;
let index;
const nodes =
metaTag(doc, "og:video", "property") || metaTag(doc, "og:video", "name");
const nodes = metaTag(doc, 'og:video', 'property') || metaTag(doc, 'og:video', 'name');
if (nodes?.length) {
nodeTypes =
metaTag(doc, "og:video:type", "property") ||
metaTag(doc, "og:video:type", "name");
metaTag(doc, 'og:video:type', 'property') || metaTag(doc, 'og:video:type', 'name');
nodeSecureUrls =
metaTag(doc, "og:video:secure_url", "property") ||
metaTag(doc, "og:video:secure_url", "name");
metaTag(doc, 'og:video:secure_url', 'property') ||
metaTag(doc, 'og:video:secure_url', 'name');
width =
metaTagContent(doc, "og:video:width", "property") ||
metaTagContent(doc, "og:video:width", "name");
metaTagContent(doc, 'og:video:width', 'property') ||
metaTagContent(doc, 'og:video:width', 'name');
height =
metaTagContent(doc, "og:video:height", "property") ||
metaTagContent(doc, "og:video:height", "name");
metaTagContent(doc, 'og:video:height', 'property') ||
metaTagContent(doc, 'og:video:height', 'name');
for (index = 0; index < nodes.length; index += 1) {
const node = nodes[index];
if (node.type === "tag") video = node.attribs.content;
if (node.type === 'tag') video = node.attribs.content;
nodeType = nodeTypes?.[index];
if (nodeType?.type === "tag") {
if (nodeType?.type === 'tag') {
videoType = nodeType ? nodeType.attribs.content : null;
}
nodeSecureUrl = nodeSecureUrls?.[index];
if (nodeSecureUrl?.type === "tag") {
if (nodeSecureUrl?.type === 'tag') {
videoSecureUrl = nodeSecureUrl ? nodeSecureUrl.attribs.content : null;
}
@@ -173,7 +171,7 @@ function getVideos(doc: cheerio.CheerioAPI) {
width,
height,
};
if (videoType && videoType.indexOf("video/") === 0) {
if (videoType && videoType.indexOf('video/') === 0) {
videos.splice(0, 0, videoObj);
} else {
videos.push(videoObj);
@@ -195,11 +193,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
let nodes: cheerio.Cheerio<cheerio.Element> | never[] = [];
let src: string | undefined;
const relSelectors = [
"rel=icon",
`rel="shortcut icon"`,
"rel=apple-touch-icon",
];
const relSelectors = ['rel=icon', `rel="shortcut icon"`, 'rel=apple-touch-icon'];
relSelectors.forEach((relSelector) => {
// look for all icon tags
@@ -208,7 +202,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
// collect all images from icon tags
if (nodes.length) {
nodes.each((_: number, node: cheerio.Element) => {
if (node.type === "tag") src = node.attribs.href;
if (node.type === 'tag') src = node.attribs.href;
if (src) {
src = new URL(rootUrl).href;
images.push(src);
@@ -228,7 +222,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
function parseImageResponse(url: string, contentType: string) {
return {
url,
mediaType: "image",
mediaType: 'image',
contentType,
favicons: [getDefaultFavicon(url)],
};
@@ -237,7 +231,7 @@ function parseImageResponse(url: string, contentType: string) {
function parseAudioResponse(url: string, contentType: string) {
return {
url,
mediaType: "audio",
mediaType: 'audio',
contentType,
favicons: [getDefaultFavicon(url)],
};
@@ -246,7 +240,7 @@ function parseAudioResponse(url: string, contentType: string) {
function parseVideoResponse(url: string, contentType: string) {
return {
url,
mediaType: "video",
mediaType: 'video',
contentType,
favicons: [getDefaultFavicon(url)],
};
@@ -255,7 +249,7 @@ function parseVideoResponse(url: string, contentType: string) {
function parseApplicationResponse(url: string, contentType: string) {
return {
url,
mediaType: "application",
mediaType: 'application',
contentType,
favicons: [getDefaultFavicon(url)],
};
@@ -265,7 +259,7 @@ function parseTextResponse(
body: string,
url: string,
options: ILinkPreviewOptions = {},
contentType?: string,
contentType?: string
) {
const doc = cheerio.load(body);
@@ -274,7 +268,7 @@ function parseTextResponse(
title: getTitle(doc),
siteName: getSiteName(doc),
description: getDescription(doc),
mediaType: getMediaType(doc) || "website",
mediaType: getMediaType(doc) || 'website',
contentType,
images: getImages(doc, url, options.imagesPropertyType),
videos: getVideos(doc),
@@ -286,21 +280,18 @@ function parseUnknownResponse(
body: string,
url: string,
options: ILinkPreviewOptions = {},
contentType?: string,
contentType?: string
) {
return parseTextResponse(body, url, options, contentType);
}
function parseResponse(
response: IPreFetchedResource,
options?: ILinkPreviewOptions,
) {
function parseResponse(response: IPreFetchedResource, options?: ILinkPreviewOptions) {
try {
let contentType = response.headers["content-type"];
let contentType = response.headers['content-type'];
// console.warn(`original content type`, contentType);
if (contentType?.indexOf(";")) {
if (contentType?.indexOf(';')) {
// eslint-disable-next-line prefer-destructuring
contentType = contentType.split(";")[0];
contentType = contentType.split(';')[0];
// console.warn(`splitting content type`, contentType);
}
@@ -334,9 +325,7 @@ function parseResponse(
return parseUnknownResponse(htmlString, response.url, options);
} catch (e) {
throw new Error(
`link-preview-js could not fetch link information ${(
e as any
).toString()}`,
`link-preview-js could not fetch link information ${(e as any).toString()}`
);
}
}
@@ -344,7 +333,7 @@ function parseResponse(
export async function getLinkPreview(text: string) {
const fetchUrl = text;
const options: FetchOptions = {
method: "GET",
method: 'GET',
timeout: 5,
responseType: ResponseType.Text,
};
@@ -352,7 +341,7 @@ export async function getLinkPreview(text: string) {
let response = await fetch(fetchUrl, options);
if (response.status > 300 && response.status < 309) {
const forwardedUrl = response.headers.location || "";
const forwardedUrl = response.headers.location || '';
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;
@@ -9,16 +10,14 @@ export async function connect(): Promise<Database> {
if (db) {
return db;
}
db = await Database.load("sqlite:lume.db");
db = await Database.load('sqlite:lume.db');
return db;
}
// get active account
export async function getActiveAccount() {
const db = await connect();
const result: any = await db.select(
"SELECT * FROM accounts WHERE is_active = 1;",
);
const result: any = await db.select('SELECT * FROM accounts WHERE is_active = 1;');
if (result.length > 0) {
return result[0];
} else {
@@ -30,7 +29,7 @@ export async function getActiveAccount() {
export async function getAccounts() {
const db = await connect();
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,
privkey: string,
follows?: string[][],
is_active?: number,
is_active?: number
) {
const db = await connect();
const res = await db.execute(
"INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);",
[npub, pubkey, privkey, follows || "", is_active || 0],
'INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);',
[npub, pubkey, privkey, follows || '', is_active || 0]
);
if (res) {
await createBlock(
0,
"Preserve your freedom",
"https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv",
'Preserve your freedom',
'https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv'
);
}
const getAccount = await getActiveAccount();
@@ -62,13 +61,13 @@ export async function createAccount(
export async function updateAccount(
column: string,
value: string | string[],
pubkey: string,
pubkey: string
) {
const db = await connect();
return await db.execute(
`UPDATE accounts SET ${column} = ? WHERE pubkey = ?;`,
[value, pubkey],
);
return await db.execute(`UPDATE accounts SET ${column} = ? WHERE pubkey = ?;`, [
value,
pubkey,
]);
}
// count total notes
@@ -82,7 +81,7 @@ export async function countTotalChannels() {
export async function countTotalNotes() {
const db = await connect();
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);
}
@@ -95,12 +94,11 @@ export async function getNotes(limit: number, offset: number) {
const notes: any = { data: null, nextCursor: 0 };
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["nextCursor"] =
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
notes['data'] = query;
notes['nextCursor'] = Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
return notes;
}
@@ -109,18 +107,14 @@ export async function getNotes(limit: number, offset: number) {
export async function getNotesByPubkey(pubkey: string) {
const db = await connect();
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;
}
// get all notes by authors
export async function getNotesByAuthors(
authors: string,
limit: number,
offset: number,
) {
export async function getNotesByAuthors(authors: string, limit: number, offset: number) {
const db = await connect();
const totalNotes = await countTotalNotes();
const nextCursor = offset + limit;
@@ -129,12 +123,11 @@ export async function getNotesByAuthors(
const notes: any = { data: null, nextCursor: 0 };
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["nextCursor"] =
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
notes['data'] = query;
notes['nextCursor'] = Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
return notes;
}
@@ -142,9 +135,7 @@ export async function getNotesByAuthors(
// get note by id
export async function getNoteByID(event_id: string) {
const db = await connect();
const result = await db.select(
`SELECT * FROM notes WHERE event_id = "${event_id}";`,
);
const result = await db.select(`SELECT * FROM notes WHERE event_id = "${event_id}";`);
return result[0];
}
@@ -155,15 +146,15 @@ export async function createNote(
kind: number,
tags: any,
content: string,
created_at: number,
created_at: number
) {
const db = await connect();
const account = await getActiveAccount();
const parentID = getParentID(tags, event_id);
return await db.execute(
"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],
'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]
);
}
@@ -171,7 +162,7 @@ export async function createNote(
export async function getReplies(parent_id: string) {
const db = await connect();
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;
}
@@ -184,30 +175,26 @@ export async function createReplyNote(
kind: number,
tags: any,
content: string,
created_at: number,
created_at: number
) {
const db = await connect();
return await db.execute(
"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],
'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]
);
}
// get all channels
export async function getChannels() {
const db = await connect();
const result: any = await db.select(
"SELECT * FROM channels ORDER BY created_at DESC;",
);
const result: any = await db.select('SELECT * FROM channels ORDER BY created_at DESC;');
return result;
}
// get channel by id
export async function getChannel(id: string) {
const db = await connect();
const result = await db.select(
`SELECT * FROM channels WHERE event_id = "${id}";`,
);
const result = await db.select(`SELECT * FROM channels WHERE event_id = "${id}";`);
return result[0];
}
@@ -218,12 +205,12 @@ export async function createChannel(
name: string,
picture: string,
about: string,
created_at: number,
created_at: number
) {
const db = await connect();
return await db.execute(
"INSERT OR IGNORE INTO channels (event_id, pubkey, name, picture, about, created_at) VALUES (?, ?, ?, ?, ?, ?);",
[event_id, pubkey, name, picture, about, created_at],
'INSERT OR IGNORE INTO channels (event_id, pubkey, name, picture, about, created_at) VALUES (?, ?, ?, ?, ?, ?);',
[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);
return await db.execute(
"UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;",
[data.name, data.picture, data.about, event_id],
'UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;',
[data.name, data.picture, data.about, event_id]
);
}
@@ -246,12 +233,12 @@ export async function createChannelMessage(
kind: number,
content: string,
tags: string[][],
created_at: number,
created_at: number
) {
const db = await connect();
return await db.execute(
"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],
'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]
);
}
@@ -259,7 +246,7 @@ export async function createChannelMessage(
export async function getChannelMessages(channel_id: string) {
const db = await connect();
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) {
const db = await connect();
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;
}
@@ -276,33 +263,29 @@ export async function getChannelUsers(channel_id: string) {
export async function getChatsByPubkey(pubkey: string) {
const db = await connect();
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 }));
return newArr;
}
// get chat messages
export async function getChatMessages(
receiver_pubkey: string,
sender_pubkey: string,
) {
export async function getChatMessages(receiver_pubkey: string, sender_pubkey: string) {
const db = await connect();
let receiver = [];
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) {
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(
(x: { created_at: number }, y: { created_at: number }) =>
x.created_at - y.created_at,
(x: { created_at: number }, y: { created_at: number }) => x.created_at - y.created_at
);
return result;
@@ -315,12 +298,12 @@ export async function createChat(
sender_pubkey: string,
content: string,
tags: string[][],
created_at: number,
created_at: number
) {
const db = await connect();
await db.execute(
"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],
'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]
);
return sender_pubkey;
}
@@ -328,26 +311,20 @@ export async function createChat(
// get setting
export async function getSetting(key: string) {
const db = await connect();
const result = await db.select(
`SELECT value FROM settings WHERE key = "${key}";`,
);
const result = await db.select(`SELECT value FROM settings WHERE key = "${key}";`);
return result[0]?.value;
}
// update setting
export async function updateSetting(key: string, value: string | number) {
const db = await connect();
return await db.execute(
`UPDATE settings SET value = "${value}" WHERE key = "${key}";`,
);
return await db.execute(`UPDATE settings SET value = "${value}" WHERE key = "${key}";`);
}
// get last login
export async function getLastLogin() {
const db = await connect();
const result = await db.select(
`SELECT value FROM settings WHERE key = "last_login";`,
);
const result = await db.select(`SELECT value FROM settings WHERE key = "last_login";`);
if (result[0]) {
return parseInt(result[0].value);
} else {
@@ -359,7 +336,7 @@ export async function getLastLogin() {
export async function updateLastLogin(value: number) {
const db = await connect();
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) {
const db = await connect();
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) {
const db = await connect();
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,
content: string,
kind: number,
status?: number,
status?: number
) {
const db = await connect();
return await db.execute(
"INSERT OR IGNORE INTO blacklist (account_id, content, kind, status) VALUES (?, ?, ?, ?);",
[account_id, content, kind, status || 1],
'INSERT OR IGNORE INTO blacklist (account_id, content, kind, status) VALUES (?, ?, ?, ?);',
[account_id, content, kind, status || 1]
);
}
@@ -397,7 +374,7 @@ export async function addToBlacklist(
export async function updateItemInBlacklist(content: string, status: number) {
const db = await connect();
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 activeAccount = await getActiveAccount();
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;
}
@@ -416,8 +393,8 @@ export async function createBlock(kind: number, title: string, content: any) {
const db = await connect();
const activeAccount = await getActiveAccount();
return await db.execute(
"INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);",
[activeAccount.id, kind, title, content],
'INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);',
[activeAccount.id, kind, title, content]
);
}
@@ -431,11 +408,11 @@ export async function removeBlock(id: string) {
export async function removeAll() {
const db = await connect();
await db.execute(`UPDATE settings SET value = "0" WHERE key = "last_login";`);
await db.execute("DELETE FROM replies;");
await db.execute("DELETE FROM notes;");
await db.execute("DELETE FROM blacklist;");
await db.execute("DELETE FROM blocks;");
await db.execute("DELETE FROM chats;");
await db.execute("DELETE FROM accounts;");
await db.execute('DELETE FROM replies;');
await db.execute('DELETE FROM notes;');
await db.execute('DELETE FROM blacklist;');
await db.execute('DELETE FROM blocks;');
await db.execute('DELETE FROM chats;');
await db.execute('DELETE FROM accounts;');
return true;
}

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
export function InactiveAccount({ data }: { data: any }) {
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 }) {
const navigate = useNavigate();
@@ -15,8 +16,8 @@ export function AppHeader({ reverse }: { reverse?: boolean }) {
return (
<div
data-tauri-drag-region
className={`shrink-0 flex h-11 w-full px-3 border-b border-zinc-900 items-center ${
reverse ? "justify-start" : "justify-end"
className={`flex h-11 w-full shrink-0 items-center border-b border-zinc-900 px-3 ${
reverse ? 'justify-start' : 'justify-end'
}`}
>
<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() {
return (
<div className="flex w-screen h-screen">
<div className="relative flex flex-row shrink-0">
<div className="flex h-screen w-screen">
<div className="relative flex shrink-0 flex-row">
<Navigation />
</div>
<div className="w-full h-full">
<div className="h-full w-full">
<Outlet />
<ScrollRestoration />
</div>

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
export function Button({
preset,
@@ -7,24 +7,24 @@ export function Button({
disabled = false,
onClick = undefined,
}: {
preset: "small" | "publish" | "large";
preset: 'small' | 'publish' | 'large';
children: ReactNode;
disabled?: boolean;
onClick?: () => void;
}) {
let preClass: string;
switch (preset) {
case "small":
case 'small':
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;
case "publish":
case 'publish':
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;
case "large":
case 'large':
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;
default:
break;
@@ -36,8 +36,8 @@ export function Button({
onClick={onClick}
disabled={disabled}
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",
preClass,
'inline-flex transform items-center justify-center gap-1 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
preClass
)}
>
{children}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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