rome -> eslint + prettier
This commit is contained in:
49
.eslintrc.js
Normal file
49
.eslintrc.js
Normal 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
9
.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
||||
.tmp
|
||||
.cache/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
**/.yarn/**
|
||||
**/.pnp.*
|
||||
/dist*/
|
||||
node_modules/
|
||||
src-tauri/
|
||||
22
.prettierrc
Normal file
22
.prettierrc
Normal 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
|
||||
}
|
||||
18
package.json
18
package.json
@@ -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
698
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
18
rome.json
18
rome.json
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/app.tsx
99
src/app.tsx
@@ -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 /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export function AuthCreateScreen() {
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export function AuthImportScreen() {
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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're a part of better future that we'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>
|
||||
|
||||
@@ -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")`,
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' : ''
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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't talk yet, let's send first message
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)`,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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)`,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'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>
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
19
src/main.tsx
19
src/main.tsx
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user