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",
|
"build": "vite build",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"add-migrate": "cd src-tauri/ && sqlx migrate add",
|
"add-migrate": "cd src-tauri/ && sqlx migrate add",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install",
|
||||||
|
"lint": "eslint ./src --fix",
|
||||||
|
"format": "prettier ./src --write"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*.{js,ts,jsx,tsx}": "rome check --apply"
|
"src/*.{ts, tsx}": "eslint --fix",
|
||||||
|
"src/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "^0.23.1",
|
"@floating-ui/react": "^0.23.1",
|
||||||
@@ -48,20 +51,29 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@tauri-apps/cli": "^1.4.0",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||||
"@types/node": "^18.16.18",
|
"@types/node": "^18.16.18",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.14",
|
||||||
"@types/react-dom": "^18.2.6",
|
"@types/react-dom": "^18.2.6",
|
||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"csstype": "^3.1.2",
|
"csstype": "^3.1.2",
|
||||||
"encoding": "^0.1.13",
|
"encoding": "^0.1.13",
|
||||||
|
"eslint": "^8.44.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.2.3",
|
"lint-staged": "^13.2.3",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"rome": "12.1.0",
|
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^4.3.9",
|
"vite": "^4.3.9",
|
||||||
|
|||||||
698
pnpm-lock.yaml
generated
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 { RouterProvider, createBrowserRouter } from 'react-router-dom';
|
||||||
import { AuthCreateScreen } from "@app/auth/create";
|
|
||||||
import { CreateStep1Screen } from "@app/auth/create/step-1";
|
import { AuthCreateScreen } from '@app/auth/create';
|
||||||
import { CreateStep2Screen } from "@app/auth/create/step-2";
|
import { CreateStep1Screen } from '@app/auth/create/step-1';
|
||||||
import { CreateStep3Screen } from "@app/auth/create/step-3";
|
import { CreateStep2Screen } from '@app/auth/create/step-2';
|
||||||
import { CreateStep4Screen } from "@app/auth/create/step-4";
|
import { CreateStep3Screen } from '@app/auth/create/step-3';
|
||||||
import { AuthImportScreen } from "@app/auth/import";
|
import { CreateStep4Screen } from '@app/auth/create/step-4';
|
||||||
import { ImportStep1Screen } from "@app/auth/import/step-1";
|
import { AuthImportScreen } from '@app/auth/import';
|
||||||
import { ImportStep2Screen } from "@app/auth/import/step-2";
|
import { ImportStep1Screen } from '@app/auth/import/step-1';
|
||||||
import { OnboardingScreen } from "@app/auth/onboarding";
|
import { ImportStep2Screen } from '@app/auth/import/step-2';
|
||||||
import { WelcomeScreen } from "@app/auth/welcome";
|
import { OnboardingScreen } from '@app/auth/onboarding';
|
||||||
import { ChannelScreen } from "@app/channel";
|
import { WelcomeScreen } from '@app/auth/welcome';
|
||||||
import { ChatScreen } from "@app/chat";
|
import { ChannelScreen } from '@app/channel';
|
||||||
import { ErrorScreen } from "@app/error";
|
import { ChatScreen } from '@app/chat';
|
||||||
import { Root } from "@app/root";
|
import { ErrorScreen } from '@app/error';
|
||||||
import { AccountSettingsScreen } from "@app/settings/account";
|
import { Root } from '@app/root';
|
||||||
import { GeneralSettingsScreen } from "@app/settings/general";
|
import { AccountSettingsScreen } from '@app/settings/account';
|
||||||
import { ShortcutsSettingsScreen } from "@app/settings/shortcuts";
|
import { GeneralSettingsScreen } from '@app/settings/general';
|
||||||
import { SpaceScreen } from "@app/space";
|
import { ShortcutsSettingsScreen } from '@app/settings/shortcuts';
|
||||||
import { TrendingScreen } from "@app/trending";
|
import { SpaceScreen } from '@app/space';
|
||||||
import { UserScreen } from "@app/user";
|
import { TrendingScreen } from '@app/trending';
|
||||||
import { AppLayout } from "@shared/appLayout";
|
import { UserScreen } from '@app/user';
|
||||||
import { AuthLayout } from "@shared/authLayout";
|
|
||||||
import { Protected } from "@shared/protected";
|
import { AppLayout } from '@shared/appLayout';
|
||||||
import { SettingsLayout } from "@shared/settingsLayout";
|
import { AuthLayout } from '@shared/authLayout';
|
||||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
import { Protected } from '@shared/protected';
|
||||||
|
import { SettingsLayout } from '@shared/settingsLayout';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: '/',
|
||||||
element: (
|
element: (
|
||||||
<Protected>
|
<Protected>
|
||||||
<Root />
|
<Root />
|
||||||
@@ -36,57 +39,57 @@ const router = createBrowserRouter([
|
|||||||
errorElement: <ErrorScreen />,
|
errorElement: <ErrorScreen />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/auth",
|
path: '/auth',
|
||||||
element: <AuthLayout />,
|
element: <AuthLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ path: "welcome", element: <WelcomeScreen /> },
|
{ path: 'welcome', element: <WelcomeScreen /> },
|
||||||
{ path: "onboarding", element: <OnboardingScreen /> },
|
{ path: 'onboarding', element: <OnboardingScreen /> },
|
||||||
{
|
{
|
||||||
path: "import",
|
path: 'import',
|
||||||
element: <AuthImportScreen />,
|
element: <AuthImportScreen />,
|
||||||
children: [
|
children: [
|
||||||
{ path: "", element: <ImportStep1Screen /> },
|
{ path: '', element: <ImportStep1Screen /> },
|
||||||
{ path: "step-2", element: <ImportStep2Screen /> },
|
{ path: 'step-2', element: <ImportStep2Screen /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "create",
|
path: 'create',
|
||||||
element: <AuthCreateScreen />,
|
element: <AuthCreateScreen />,
|
||||||
children: [
|
children: [
|
||||||
{ path: "", element: <CreateStep1Screen /> },
|
{ path: '', element: <CreateStep1Screen /> },
|
||||||
{ path: "step-2", element: <CreateStep2Screen /> },
|
{ path: 'step-2', element: <CreateStep2Screen /> },
|
||||||
{ path: "step-3", element: <CreateStep3Screen /> },
|
{ path: 'step-3', element: <CreateStep3Screen /> },
|
||||||
{ path: "step-4", element: <CreateStep4Screen /> },
|
{ path: 'step-4', element: <CreateStep4Screen /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/app",
|
path: '/app',
|
||||||
element: (
|
element: (
|
||||||
<Protected>
|
<Protected>
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
</Protected>
|
</Protected>
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{ path: "space", element: <SpaceScreen /> },
|
{ path: 'space', element: <SpaceScreen /> },
|
||||||
{ path: "trending", element: <TrendingScreen /> },
|
{ path: 'trending', element: <TrendingScreen /> },
|
||||||
{ path: "user/:pubkey", element: <UserScreen /> },
|
{ path: 'user/:pubkey', element: <UserScreen /> },
|
||||||
{ path: "chat/:pubkey", element: <ChatScreen /> },
|
{ path: 'chat/:pubkey', element: <ChatScreen /> },
|
||||||
{ path: "channel/:id", element: <ChannelScreen /> },
|
{ path: 'channel/:id', element: <ChannelScreen /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/settings",
|
path: '/settings',
|
||||||
element: (
|
element: (
|
||||||
<Protected>
|
<Protected>
|
||||||
<SettingsLayout />
|
<SettingsLayout />
|
||||||
</Protected>
|
</Protected>
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{ path: "general", element: <GeneralSettingsScreen /> },
|
{ path: 'general', element: <GeneralSettingsScreen /> },
|
||||||
{ path: "shortcuts", element: <ShortcutsSettingsScreen /> },
|
{ path: 'shortcuts', element: <ShortcutsSettingsScreen /> },
|
||||||
{ path: "account", element: <AccountSettingsScreen /> },
|
{ path: 'account', element: <AccountSettingsScreen /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from '@shared/image';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
|
||||||
|
|
||||||
export function User({
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
pubkey,
|
|
||||||
fallback,
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
}: { pubkey: string; fallback?: string }) {
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
|
export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }) {
|
||||||
const { status, user } = useProfile(pubkey, fallback);
|
const { status, user } = useProfile(pubkey, fallback);
|
||||||
|
|
||||||
if (status === "loading") {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative h-10 w-10 shrink-0 rounded-md bg-zinc-800 animate-pulse" />
|
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-zinc-800" />
|
||||||
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
||||||
<span className="w-1/2 h-4 rounded bg-zinc-800 animate-pulse" />
|
<span className="h-4 w-1/2 animate-pulse rounded bg-zinc-800" />
|
||||||
<span className="w-1/3 h-3 rounded bg-zinc-800 animate-pulse" />
|
<span className="h-3 w-1/3 animate-pulse rounded bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
export function AuthCreateScreen() {
|
export function AuthCreateScreen() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { createAccount, createBlock } from "@libs/storage";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Button } from "@shared/button";
|
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||||
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from "@shared/icons";
|
import { useMemo, useState } from 'react';
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
|
|
||||||
import { useMemo, useState } from "react";
|
import { createAccount } from '@libs/storage';
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
import { Button } from '@shared/button';
|
||||||
|
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function CreateStep1Screen() {
|
export function CreateStep1Screen() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [type, setType] = useState("password");
|
const [type, setType] = useState('password');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const privkey = useMemo(() => generatePrivateKey(), []);
|
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||||
@@ -20,19 +22,25 @@ export function CreateStep1Screen() {
|
|||||||
|
|
||||||
// toggle private key
|
// toggle private key
|
||||||
const showPrivateKey = () => {
|
const showPrivateKey = () => {
|
||||||
if (type === "password") {
|
if (type === 'password') {
|
||||||
setType("text");
|
setType('text');
|
||||||
} else {
|
} else {
|
||||||
setType("password");
|
setType('password');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const account = useMutation({
|
const account = useMutation({
|
||||||
mutationFn: (data: any) => {
|
mutationFn: (data: {
|
||||||
|
npub: string;
|
||||||
|
pubkey: string;
|
||||||
|
privkey: string;
|
||||||
|
follows: null | string[][];
|
||||||
|
is_active: number;
|
||||||
|
}) => {
|
||||||
return createAccount(data.npub, data.pubkey, data.privkey, null, 1);
|
return createAccount(data.npub, data.pubkey, data.privkey, null, 1);
|
||||||
},
|
},
|
||||||
onSuccess: (data: any) => {
|
onSuccess: (data) => {
|
||||||
queryClient.setQueryData(["currentAccount"], data);
|
queryClient.setQueryData(['currentAccount'], data);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +56,7 @@ export function CreateStep1Screen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// redirect to next step
|
// redirect to next step
|
||||||
setTimeout(() => navigate("/auth/create/step-2", { replace: true }), 1200);
|
setTimeout(() => navigate('/auth/create/step-2', { replace: true }), 1200);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,32 +68,28 @@ export function CreateStep1Screen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-base font-semibold text-zinc-400">
|
<span className="text-base font-semibold text-zinc-400">Public Key</span>
|
||||||
Public Key
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
value={npub}
|
value={npub}
|
||||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-base font-semibold text-zinc-400">
|
<span className="text-base font-semibold text-zinc-400">Private Key</span>
|
||||||
Private Key
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
type={type}
|
type={type}
|
||||||
value={nsec}
|
value={nsec}
|
||||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showPrivateKey()}
|
onClick={() => showPrivateKey()}
|
||||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
{type === "password" ? (
|
{type === 'password' ? (
|
||||||
<EyeOffIcon
|
<EyeOffIcon
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
@@ -105,7 +109,7 @@ export function CreateStep1Screen() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
) : (
|
) : (
|
||||||
"Continue →"
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { AvatarUploader } from "@shared/avatarUploader";
|
import { useState } from 'react';
|
||||||
import { BannerUploader } from "@shared/bannerUploader";
|
import { useForm } from 'react-hook-form';
|
||||||
import { LoaderIcon } from "@shared/icons";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Image } from "@shared/image";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { AvatarUploader } from '@shared/avatarUploader';
|
||||||
import { useOnboarding } from "@stores/onboarding";
|
import { BannerUploader } from '@shared/bannerUploader';
|
||||||
import { useState } from "react";
|
import { LoaderIcon } from '@shared/icons';
|
||||||
import { useForm } from "react-hook-form";
|
import { Image } from '@shared/image';
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
|
||||||
export function CreateStep2Screen() {
|
export function CreateStep2Screen() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const createProfile = useOnboarding((state: any) => state.createProfile);
|
const createProfile = useOnboarding((state: any) => state.createProfile);
|
||||||
|
|
||||||
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
||||||
const [banner, setBanner] = useState("");
|
const [banner, setBanner] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -33,57 +35,52 @@ export function CreateStep2Screen() {
|
|||||||
};
|
};
|
||||||
createProfile(profile);
|
createProfile(profile);
|
||||||
// redirect to next step
|
// redirect to next step
|
||||||
setTimeout(
|
setTimeout(() => navigate('/auth/create/step-3', { replace: true }), 1200);
|
||||||
() => navigate("/auth/create/step-3", { replace: true }),
|
|
||||||
1200,
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
console.log("error");
|
console.log('error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="text-xl font-semibold text-zinc-100">
|
<h1 className="text-xl font-semibold text-zinc-100">Create your profile</h1>
|
||||||
Create your profile
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
<div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col mb-0">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||||
<input
|
<input
|
||||||
type={"hidden"}
|
type={'hidden'}
|
||||||
{...register("picture")}
|
{...register('picture')}
|
||||||
value={picture}
|
value={picture}
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type={"hidden"}
|
type={'hidden'}
|
||||||
{...register("banner")}
|
{...register('banner')}
|
||||||
value={banner}
|
value={banner}
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative w-full h-44 bg-zinc-800">
|
<div className="relative h-44 w-full bg-zinc-800">
|
||||||
<Image
|
<Image
|
||||||
src={banner}
|
src={banner}
|
||||||
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
|
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
|
||||||
alt="user's banner"
|
alt="user's banner"
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full">
|
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
<BannerUploader setBanner={setBanner} />
|
<BannerUploader setBanner={setBanner} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 mb-5">
|
<div className="mb-5 px-4">
|
||||||
<div className="z-10 relative h-14 w-14 -mt-7">
|
<div className="relative z-10 -mt-7 h-14 w-14">
|
||||||
<Image
|
<Image
|
||||||
src={picture}
|
src={picture}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt="user's avatar"
|
alt="user's avatar"
|
||||||
className="h-14 w-14 object-cover ring-2 ring-zinc-900 rounded-lg"
|
className="h-14 w-14 rounded-lg object-cover ring-2 ring-zinc-900"
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full">
|
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
<AvatarUploader setPicture={setPicture} />
|
<AvatarUploader setPicture={setPicture} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,51 +88,60 @@ export function CreateStep2Screen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Name *
|
Name *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={"text"}
|
type={'text'}
|
||||||
{...register("name", {
|
{...register('name', {
|
||||||
required: true,
|
required: true,
|
||||||
minLength: 4,
|
minLength: 4,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="about"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Bio
|
Bio
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
{...register("about")}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="resize-none relative h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="website"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Website
|
Website
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={"text"}
|
type={'text'}
|
||||||
{...register("website", {
|
{...register('website', {
|
||||||
required: false,
|
required: false,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
) : (
|
) : (
|
||||||
"Continue →"
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { Button } from "@shared/button";
|
import { Body, fetch } from '@tauri-apps/api/http';
|
||||||
import { LoaderIcon } from "@shared/icons";
|
import { useContext, useState } from 'react';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useOnboarding } from "@stores/onboarding";
|
|
||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
import { Button } from '@shared/button';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { LoaderIcon } from '@shared/icons';
|
||||||
import { useContext, useState } from "react";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function CreateStep3Screen() {
|
export function CreateStep3Screen() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -15,23 +18,23 @@ export function CreateStep3Screen() {
|
|||||||
|
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const createNIP05 = async () => {
|
const createNIP05 = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const response = await fetch("https://lume.nu/api/user-create", {
|
const response = await fetch('https://lume.nu/api/user-create', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
},
|
},
|
||||||
body: Body.json({
|
body: Body.json({
|
||||||
username: username,
|
username: username,
|
||||||
pubkey: account.pubkey,
|
pubkey: account.pubkey,
|
||||||
lightningAddress: "",
|
lightningAddress: '',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,23 +54,21 @@ export function CreateStep3Screen() {
|
|||||||
event.publish();
|
event.publish();
|
||||||
|
|
||||||
// redirect to step 4
|
// redirect to step 4
|
||||||
navigate("/auth/create/step-4", { replace: true });
|
navigate('/auth/create/step-4', { replace: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
console.error("Error:", error);
|
console.error('Error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="text-xl font-semibold text-zinc-100">
|
<h1 className="text-xl font-semibold text-zinc-100">Create your Lume ID</h1>
|
||||||
Create your Lume ID
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-col justify-center items-center gap-4">
|
<div className="flex w-full flex-col items-center justify-center gap-4">
|
||||||
<div className="w-full inline-flex items-center justify-center gap-2 rounded-lg bg-zinc-800">
|
<div className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-zinc-800">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
@@ -76,11 +77,9 @@ export function CreateStep3Screen() {
|
|||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder="satoshi"
|
placeholder="satoshi"
|
||||||
className="relative w-full py-3 pl-3.5 !outline-none placeholder:text-zinc-500 bg-transparent text-zinc-100"
|
className="relative w-full bg-transparent py-3 pl-3.5 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-fuchsia-500 font-semibold pr-3.5">
|
<span className="pr-3.5 font-semibold text-fuchsia-500">@lume.nu</span>
|
||||||
@lume.nu
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
preset="large"
|
preset="large"
|
||||||
@@ -90,7 +89,7 @@ export function CreateStep3Screen() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
) : (
|
) : (
|
||||||
"Continue →"
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,110 +1,114 @@
|
|||||||
import { User } from "@app/auth/components/user";
|
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { updateAccount } from "@libs/storage";
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { useContext, useState } from 'react';
|
||||||
import { CheckCircleIcon, LoaderIcon } from "@shared/icons";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { User } from '@app/auth/components/user';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
import { arrayToNIP02 } from "@utils/transform";
|
import { updateAccount } from '@libs/storage';
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
import { arrayToNIP02 } from '@utils/transform';
|
||||||
|
|
||||||
const INITIAL_LIST = [
|
const INITIAL_LIST = [
|
||||||
{
|
{
|
||||||
pubkey: "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
|
pubkey: '82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98",
|
pubkey: 'a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
|
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0",
|
pubkey: 'c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
|
pubkey: '6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411",
|
pubkey: 'e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15",
|
pubkey: 'c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42",
|
pubkey: 'e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
|
pubkey: '84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898",
|
pubkey: '703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce",
|
pubkey: 'bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
|
pubkey: '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965",
|
pubkey: 'c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6",
|
pubkey: 'c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3",
|
pubkey: '6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63",
|
pubkey: '50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594",
|
pubkey: '3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "6e3f51664e19e082df5217fd4492bb96907405a0b27028671dd7f297b688608c",
|
pubkey: '6e3f51664e19e082df5217fd4492bb96907405a0b27028671dd7f297b688608c',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884",
|
pubkey: '2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24",
|
pubkey: '3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f",
|
pubkey: 'eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479",
|
pubkey: 'be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f",
|
pubkey: 'a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b",
|
pubkey: '1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5",
|
pubkey: 'c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
pubkey: '460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "7f3b464b9ff3623630485060cbda3a7790131c5339a7803bde8feb79a5e1b06a",
|
pubkey: '7f3b464b9ff3623630485060cbda3a7790131c5339a7803bde8feb79a5e1b06a',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27",
|
pubkey: 'b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2",
|
pubkey: 'e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14",
|
pubkey: 'ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pubkey: "ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609",
|
pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -117,10 +121,10 @@ export function CreateStep4Screen() {
|
|||||||
const [follows, setFollows] = useState([]);
|
const [follows, setFollows] = useState([]);
|
||||||
|
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
const { status, data } = useQuery(["trending-profiles"], async () => {
|
const { status, data } = useQuery(['trending-profiles'], async () => {
|
||||||
const res = await fetch("https://api.nostr.band/v0/trending/profiles");
|
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Error");
|
throw new Error('Error');
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
@@ -135,10 +139,10 @@ export function CreateStep4Screen() {
|
|||||||
|
|
||||||
const update = useMutation({
|
const update = useMutation({
|
||||||
mutationFn: (follows: any) => {
|
mutationFn: (follows: any) => {
|
||||||
return updateAccount("follows", follows, account.pubkey);
|
return updateAccount('follows', follows, account.pubkey);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,7 +157,7 @@ export function CreateStep4Screen() {
|
|||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
const event = new NDKEvent(ndk);
|
||||||
// build event
|
// build event
|
||||||
event.content = "";
|
event.content = '';
|
||||||
event.kind = 3;
|
event.kind = 3;
|
||||||
event.pubkey = account.pubkey;
|
event.pubkey = account.pubkey;
|
||||||
event.tags = tags;
|
event.tags = tags;
|
||||||
@@ -164,9 +168,9 @@ export function CreateStep4Screen() {
|
|||||||
update.mutate([...follows, account.pubkey]);
|
update.mutate([...follows, account.pubkey]);
|
||||||
|
|
||||||
// redirect to next step
|
// redirect to next step
|
||||||
setTimeout(() => navigate("/auth/onboarding", { replace: true }), 1200);
|
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
|
||||||
} catch {
|
} catch {
|
||||||
console.log("error");
|
console.log('error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,40 +184,35 @@ export function CreateStep4Screen() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
<div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400">
|
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400">
|
||||||
Follow at least
|
Follow at least
|
||||||
<span className="text-fuchsia-500 font-semibold">
|
<span className="font-semibold text-fuchsia-500">
|
||||||
{follows.length}/10
|
{follows.length}/10
|
||||||
</span>{" "}
|
</span>{' '}
|
||||||
plebs
|
plebs
|
||||||
</div>
|
</div>
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="py-2 px-4 w-full h-11 inline-flex items-center justify-center">
|
<div className="inline-flex h-11 w-full items-center justify-center px-4 py-2">
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
|
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
|
||||||
{list.map(
|
{list.map((item: { pubkey: string; profile: { content: string } }) => (
|
||||||
(item: { pubkey: string; profile: { content: string } }) => (
|
|
||||||
<button
|
<button
|
||||||
key={item.pubkey}
|
key={item.pubkey}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleFollow(item.pubkey)}
|
onClick={() => toggleFollow(item.pubkey)}
|
||||||
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
|
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
|
||||||
>
|
>
|
||||||
<User
|
<User pubkey={item.pubkey} fallback={item.profile?.content} />
|
||||||
pubkey={item.pubkey}
|
|
||||||
fallback={item.profile?.content}
|
|
||||||
/>
|
|
||||||
{follows.includes(item.pubkey) && (
|
{follows.includes(item.pubkey) && (
|
||||||
<div>
|
<div>
|
||||||
<CheckCircleIcon className="w-4 h-4 text-green-400" />
|
<CheckCircleIcon className="h-4 w-4 text-green-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
),
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -221,12 +220,12 @@ export function CreateStep4Screen() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => submit()}
|
onClick={() => submit()}
|
||||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
) : (
|
) : (
|
||||||
"Finish →"
|
'Finish →'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
export function AuthImportScreen() {
|
export function AuthImportScreen() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { createAccount, createBlock } from "@libs/storage";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { LoaderIcon } from "@shared/icons";
|
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useState } from 'react';
|
||||||
import { getPublicKey, nip19 } from "nostr-tools";
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
import { useState } from "react";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Resolver, useForm } from "react-hook-form";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { createAccount } from '@libs/storage';
|
||||||
|
|
||||||
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -16,8 +18,8 @@ const resolver: Resolver<FormValues> = async (values) => {
|
|||||||
errors: !values.key
|
errors: !values.key
|
||||||
? {
|
? {
|
||||||
key: {
|
key: {
|
||||||
type: "required",
|
type: 'required',
|
||||||
message: "This is required.",
|
message: 'This is required.',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
@@ -35,7 +37,7 @@ export function ImportStep1Screen() {
|
|||||||
return createAccount(data.npub, data.pubkey, data.privkey, null, 1);
|
return createAccount(data.npub, data.pubkey, data.privkey, null, 1);
|
||||||
},
|
},
|
||||||
onSuccess: (data: any) => {
|
onSuccess: (data: any) => {
|
||||||
queryClient.setQueryData(["currentAccount"], data);
|
queryClient.setQueryData(['currentAccount'], data);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,12 +52,12 @@ export function ImportStep1Screen() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
let privkey = data["key"];
|
let privkey = data['key'];
|
||||||
if (privkey.substring(0, 4) === "nsec") {
|
if (privkey.substring(0, 4) === 'nsec') {
|
||||||
privkey = nip19.decode(privkey).data;
|
privkey = nip19.decode(privkey).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof getPublicKey(privkey) === "string") {
|
if (typeof getPublicKey(privkey) === 'string') {
|
||||||
const pubkey = getPublicKey(privkey);
|
const pubkey = getPublicKey(privkey);
|
||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
|
||||||
@@ -69,15 +71,12 @@ export function ImportStep1Screen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// redirect to step 2
|
// redirect to step 2
|
||||||
setTimeout(
|
setTimeout(() => navigate('/auth/import/step-2', { replace: true }), 1200);
|
||||||
() => navigate("/auth/import/step-2", { replace: true }),
|
|
||||||
1200,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError("key", {
|
setError('key', {
|
||||||
type: "custom",
|
type: 'custom',
|
||||||
message: "Private Key is invalid, please check again",
|
message: 'Private Key is invalid, please check again',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -91,10 +90,10 @@ export function ImportStep1Screen() {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<input
|
<input
|
||||||
{...register("key", { required: true, minLength: 32 })}
|
{...register('key', { required: true, minLength: 32 })}
|
||||||
type={"password"}
|
type={'password'}
|
||||||
placeholder="Paste private key here..."
|
placeholder="Paste private key here..."
|
||||||
className="relative w-full rounded-lg px-3 py-3 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative w-full rounded-lg bg-zinc-800 px-3 py-3 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-base text-red-400">
|
<span className="text-base text-red-400">
|
||||||
{errors.key && <p>{errors.key.message}</p>}
|
{errors.key && <p>{errors.key.message}</p>}
|
||||||
@@ -104,12 +103,12 @@ export function ImportStep1Screen() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
) : (
|
) : (
|
||||||
"Continue →"
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { User } from "@app/auth/components/user";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { updateAccount } from "@libs/storage";
|
import { useContext, useState } from 'react';
|
||||||
import { Button } from "@shared/button";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { LoaderIcon } from "@shared/icons";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { User } from '@app/auth/components/user';
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { updateAccount } from '@libs/storage';
|
||||||
import { setToArray } from "@utils/transform";
|
|
||||||
import { useContext, useState } from "react";
|
import { Button } from '@shared/button';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
import { setToArray } from '@utils/transform';
|
||||||
|
|
||||||
export function ImportStep2Screen() {
|
export function ImportStep2Screen() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -19,10 +23,10 @@ export function ImportStep2Screen() {
|
|||||||
|
|
||||||
const update = useMutation({
|
const update = useMutation({
|
||||||
mutationFn: (follows: any) => {
|
mutationFn: (follows: any) => {
|
||||||
return updateAccount("follows", follows, account.pubkey);
|
return updateAccount('follows', follows, account.pubkey);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,9 +45,9 @@ export function ImportStep2Screen() {
|
|||||||
update.mutate([...followsList, account.pubkey]);
|
update.mutate([...followsList, account.pubkey]);
|
||||||
|
|
||||||
// redirect to next step
|
// redirect to next step
|
||||||
setTimeout(() => navigate("/auth/onboarding", { replace: true }), 1200);
|
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
|
||||||
} catch {
|
} catch {
|
||||||
console.log("error");
|
console.log('error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,17 +55,17 @@ export function ImportStep2Screen() {
|
|||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="text-xl font-semibold">
|
<h1 className="text-xl font-semibold">
|
||||||
{loading ? "Creating..." : "Continue with"}
|
{loading ? 'Creating...' : 'Continue with'}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4">
|
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
|
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" />
|
<div className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" />
|
||||||
<p className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
|
<div className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +76,7 @@ export function ImportStep2Screen() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
) : (
|
) : (
|
||||||
"Continue →"
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { usePublish } from "@libs/ndk";
|
import { useState } from 'react';
|
||||||
import { LoaderIcon } from "@shared/icons";
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
|
||||||
import { User } from "@shared/user";
|
import { usePublish } from '@libs/ndk';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
import { useState } from "react";
|
import { LoaderIcon } from '@shared/icons';
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function OnboardingScreen() {
|
export function OnboardingScreen() {
|
||||||
const publish = usePublish();
|
const publish = usePublish();
|
||||||
@@ -20,13 +23,13 @@ export function OnboardingScreen() {
|
|||||||
// publish event
|
// publish event
|
||||||
publish({
|
publish({
|
||||||
content:
|
content:
|
||||||
"Running Lume, fighting for better future, join us here: https://lume.nu",
|
'Running Lume, fighting for better future, join us here: https://lume.nu',
|
||||||
kind: 1,
|
kind: 1,
|
||||||
tags: [],
|
tags: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// redirect to home
|
// redirect to home
|
||||||
setTimeout(() => navigate("/", { replace: true }), 1200);
|
setTimeout(() => navigate('/', { replace: true }), 1200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
@@ -40,27 +43,24 @@ export function OnboardingScreen() {
|
|||||||
👋 Hello, welcome you to Lume
|
👋 Hello, welcome you to Lume
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-zinc-300">
|
<p className="text-sm text-zinc-300">
|
||||||
You're a part of better future that we're fighting
|
You're a part of better future that we're fighting
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-zinc-300">
|
<p className="text-sm text-zinc-300">
|
||||||
If Lume gets your attention, please help us spread via button below
|
If Lume gets your attention, please help us spread via button below
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full border-t border-zinc-800/50 bg-zinc-900 rounded-xl">
|
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="h-min w-full px-5 py-3">
|
<div className="h-min w-full px-5 py-3">
|
||||||
{status === "success" && (
|
{status === 'success' && (
|
||||||
<User
|
<User pubkey={account.pubkey} time={Math.floor(Date.now() / 1000)} />
|
||||||
pubkey={account.pubkey}
|
|
||||||
time={Math.floor(Date.now() / 1000)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div className="-mt-6 pl-[49px] select-text whitespace-pre-line break-words text-base text-zinc-100">
|
<div className="-mt-6 select-text whitespace-pre-line break-words pl-[49px] text-base text-zinc-100">
|
||||||
<p>Running Lume, fighting for better future</p>
|
<p>Running Lume, fighting for better future</p>
|
||||||
<p>
|
<p>
|
||||||
join us here:{" "}
|
join us here:{' '}
|
||||||
<a
|
<a
|
||||||
href="https://lume.nu"
|
href="https://lume.nu"
|
||||||
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
className="font-normal text-fuchsia-500 no-underline hover:text-fuchsia-600"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
@@ -70,11 +70,11 @@ export function OnboardingScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 w-full flex flex-col gap-2">
|
<div className="mt-4 flex w-full flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => submit()}
|
onClick={() => submit()}
|
||||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
@@ -86,7 +86,7 @@ export function OnboardingScreen() {
|
|||||||
<>
|
<>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Publish</span>
|
<span>Publish</span>
|
||||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
import { Link } from 'react-router-dom';
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
|
||||||
export function WelcomeScreen() {
|
export function WelcomeScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full grid grid-cols-12 gap-4 px-4 py-4">
|
<div className="grid h-full w-full grid-cols-12 gap-4 px-4 py-4">
|
||||||
<div className="col-span-5 border-t border-zinc-800/50 bg-zinc-900 rounded-xl flex flex-col">
|
<div className="col-span-5 flex flex-col rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="w-full h-full flex flex-col justify-center px-4 py-4 gap-2">
|
<div className="flex h-full w-full flex-col justify-center gap-2 px-4 py-4">
|
||||||
<h1 className="text-zinc-700 text-4xl font-bold leading-none text-transparent">
|
<h1 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
|
||||||
Preserve your <span className="text-fuchsia-300">freedom</span>
|
Preserve your <span className="text-fuchsia-300">freedom</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className="text-zinc-700 text-4xl font-bold leading-none text-transparent">
|
<h2 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
|
||||||
Protect your <span className="text-red-300">future</span>
|
Protect your <span className="text-red-300">future</span>
|
||||||
</h2>
|
</h2>
|
||||||
<h3 className="text-zinc-700 text-4xl font-bold leading-none text-transparent">
|
<h3 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
|
||||||
Stack <span className="text-orange-300">bitcoin</span>
|
Stack <span className="text-orange-300">bitcoin</span>
|
||||||
</h3>
|
</h3>
|
||||||
<h3 className="text-zinc-700 text-4xl font-bold leading-none text-transparent">
|
<h3 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
|
||||||
Use <span className="text-purple-300">nostr</span>
|
Use <span className="text-purple-300">nostr</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto w-full flex flex-col gap-2 px-4 py-4">
|
<div className="mt-auto flex w-full flex-col gap-2 px-4 py-4">
|
||||||
<Link
|
<Link
|
||||||
to="/auth/import"
|
to="/auth/import"
|
||||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Login with private key</span>
|
<span>Login with private key</span>
|
||||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/auth/create"
|
to="/auth/create"
|
||||||
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg px-6 font-medium text-zinc-200 bg-zinc-800 hover:bg-zinc-700"
|
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-zinc-800 px-6 font-medium text-zinc-200 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
Create new key
|
Create new key
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="col-span-5 bg-zinc-900 rounded-xl bg-cover bg-center"
|
className="col-span-5 rounded-xl bg-zinc-900 bg-cover bg-center"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url("https://void.cat/d/Ps1b36vu5pdkEA2w75usuB")`,
|
backgroundImage: `url("https://void.cat/d/Ps1b36vu5pdkEA2w75usuB")`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="col-span-2 bg-zinc-900 rounded-xl bg-cover bg-center"
|
className="col-span-2 rounded-xl bg-zinc-900 bg-cover bg-center"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url("https://void.cat/d/5FdJcBP5ZXKAjYqV8hpcp3")`,
|
backgroundImage: `url("https://void.cat/d/5FdJcBP5ZXKAjYqV8hpcp3")`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { MutedItem } from "@app/channel/components/mutedItem";
|
import { Popover, Transition } from '@headlessui/react';
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Fragment } from 'react';
|
||||||
import { MuteIcon } from "@shared/icons";
|
|
||||||
import { Fragment } from "react";
|
import { MutedItem } from '@app/channel/components/mutedItem';
|
||||||
|
|
||||||
|
import { MuteIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function ChannelBlackList({ blacklist }: { blacklist: any }) {
|
export function ChannelBlackList({ blacklist }: { blacklist: any }) {
|
||||||
return (
|
return (
|
||||||
@@ -10,9 +12,7 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 focus:outline-none ${
|
className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 focus:outline-none ${
|
||||||
open
|
open ? 'bg-zinc-800 hover:bg-zinc-700' : 'bg-zinc-900 hover:bg-zinc-800'
|
||||||
? "bg-zinc-800 hover:bg-zinc-700"
|
|
||||||
: "bg-zinc-900 hover:bg-zinc-800"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<MuteIcon
|
<MuteIcon
|
||||||
@@ -31,15 +31,15 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
|
|||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-1 w-screen max-w-xs transform px-4 sm:px-0">
|
<Popover.Panel className="absolute right-0 z-10 mt-1 w-screen max-w-xs transform px-4 sm:px-0">
|
||||||
<div className="flex flex-col gap-2 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900 shadow-popover">
|
<div className="shadow-popover flex flex-col gap-2 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900">
|
||||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 p-3">
|
<div className="h-min w-full shrink-0 border-b border-zinc-800 p-3">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<h3 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text font-semibold leading-none text-transparent">
|
<h3 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text font-semibold leading-none text-transparent">
|
||||||
Your muted list
|
Your muted list
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-base leading-tight text-zinc-400">
|
<p className="text-base leading-tight text-zinc-400">
|
||||||
Currently, unmute only affect locally, when you move to
|
Currently, unmute only affect locally, when you move to new client,
|
||||||
new client, muted list will loaded again
|
muted list will loaded again
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { createChannel } from "@libs/storage";
|
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { AvatarUploader } from "@shared/avatarUploader";
|
import { Fragment, useContext, useEffect, useState } from 'react';
|
||||||
import { CancelIcon, LoaderIcon, PlusIcon } from "@shared/icons";
|
import { useForm } from 'react-hook-form';
|
||||||
import { Image } from "@shared/image";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { createChannel } from '@libs/storage';
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { dateToUnix } from "@utils/date";
|
import { AvatarUploader } from '@shared/avatarUploader';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { CancelIcon, LoaderIcon, PlusIcon } from '@shared/icons';
|
||||||
import { Fragment, useContext, useEffect, useState } from "react";
|
import { Image } from '@shared/image';
|
||||||
import { useForm } from "react-hook-form";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/date';
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function ChannelCreateModal() {
|
export function ChannelCreateModal() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -48,11 +52,11 @@ export function ChannelCreateModal() {
|
|||||||
event.name,
|
event.name,
|
||||||
event.picture,
|
event.picture,
|
||||||
event.about,
|
event.about,
|
||||||
event.created_at,
|
event.created_at
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["channels"] });
|
queryClient.invalidateQueries({ queryKey: ['channels'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,12 +96,12 @@ export function ChannelCreateModal() {
|
|||||||
navigate(`/app/channel/${event.id}`);
|
navigate(`/app/channel/${event.id}`);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("error: ", e);
|
console.log('error: ', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue("picture", image);
|
setValue('picture', image);
|
||||||
}, [setValue, image]);
|
}, [setValue, image]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -152,34 +156,30 @@ export function ChannelCreateModal() {
|
|||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<CancelIcon
|
<CancelIcon width={20} height={20} className="text-zinc-300" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
||||||
Channels are freedom square, everyone can speech freely,
|
Channels are freedom square, everyone can speech freely, no one can
|
||||||
no one can stop you or deceive what to speech
|
stop you or deceive what to speech
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex h-full w-full flex-col gap-4 mb-0"
|
className="mb-0 flex h-full w-full flex-col gap-4"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type={"hidden"}
|
type={'hidden'}
|
||||||
{...register("picture")}
|
{...register('picture')}
|
||||||
value={image}
|
value={image}
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
<span className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
||||||
Picture
|
Picture
|
||||||
</label>
|
</span>
|
||||||
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
||||||
<Image
|
<Image
|
||||||
src={image}
|
src={image}
|
||||||
@@ -188,32 +188,38 @@ export function ChannelCreateModal() {
|
|||||||
className="relative z-10 h-11 w-11 rounded-md"
|
className="relative z-10 h-11 w-11 rounded-md"
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-3 right-3 z-10">
|
<div className="absolute bottom-3 right-3 z-10">
|
||||||
<AvatarUploader valueState={setImage} />
|
<AvatarUploader setPicture={setImage} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Channel name *
|
Channel name *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={"text"}
|
type={'text'}
|
||||||
{...register("name", {
|
{...register('name', {
|
||||||
required: true,
|
required: true,
|
||||||
minLength: 4,
|
minLength: 4,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="about"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Description
|
Description
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
{...register("about")}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative resize-none h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-20 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
|
<div className="flex h-20 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
|
||||||
@@ -222,8 +228,8 @@ export function ChannelCreateModal() {
|
|||||||
Encrypted
|
Encrypted
|
||||||
</span>
|
</span>
|
||||||
<p className="w-4/5 text-sm leading-none text-zinc-400">
|
<p className="w-4/5 text-sm leading-none text-zinc-400">
|
||||||
All messages are encrypted and only invited members
|
All messages are encrypted and only invited members can view and
|
||||||
can view and send message
|
send message
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -242,12 +248,12 @@ export function ChannelCreateModal() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex items-center justify-center gap-1 transform active:translate-y-1 disabled:pointer-events-none disabled:opacity-50 focus:outline-none h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
) : (
|
) : (
|
||||||
"Create channel →"
|
'Create channel →'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
|
import { NavLink } from 'react-router-dom';
|
||||||
import { NavLink } from "react-router-dom";
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile';
|
||||||
|
|
||||||
export function ChannelsListItem({ data }: { data: any }) {
|
export function ChannelsListItem({ data }: { data: any }) {
|
||||||
const channel = useChannelProfile(data.event_id);
|
const channel = useChannelProfile(data.event_id);
|
||||||
@@ -10,19 +11,19 @@ export function ChannelsListItem({ data }: { data: any }) {
|
|||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
|
||||||
isActive ? "bg-zinc-900/50 text-zinc-100" : "",
|
isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<span className="text-xs text-zinc-100">#</span>
|
<span className="text-xs text-zinc-100">#</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full inline-flex items-center justify-between">
|
<div className="inline-flex w-full items-center justify-between">
|
||||||
<h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5>
|
<h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{data.new_messages && (
|
{data.new_messages && (
|
||||||
<span className="inline-flex items-center justify-center rounded bg-fuchsia-400/10 w-8 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">
|
<span className="inline-flex w-8 items-center justify-center rounded bg-fuchsia-400/10 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">
|
||||||
{data.new_messages}
|
{data.new_messages}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { ChannelCreateModal } from "@app/channel/components/createModal";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { ChannelsListItem } from "@app/channel/components/item";
|
|
||||||
import { getChannels } from "@libs/storage";
|
import { ChannelCreateModal } from '@app/channel/components/createModal';
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { ChannelsListItem } from '@app/channel/components/item';
|
||||||
|
|
||||||
|
import { getChannels } from '@libs/storage';
|
||||||
|
|
||||||
export function ChannelsList() {
|
export function ChannelsList() {
|
||||||
const {
|
const {
|
||||||
@@ -9,7 +11,7 @@ export function ChannelsList() {
|
|||||||
data: channels,
|
data: channels,
|
||||||
isFetching,
|
isFetching,
|
||||||
} = useQuery(
|
} = useQuery(
|
||||||
["channels"],
|
['channels'],
|
||||||
async () => {
|
async () => {
|
||||||
return await getChannels();
|
return await getChannels();
|
||||||
},
|
},
|
||||||
@@ -17,12 +19,12 @@ export function ChannelsList() {
|
|||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<>
|
<>
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from '@shared/image';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
|
||||||
export function Member({ pubkey }: { pubkey: string }) {
|
export function Member({ pubkey }: { pubkey: string }) {
|
||||||
const { user, isError, isLoading } = useProfile(pubkey);
|
const { user, isError, isLoading } = useProfile(pubkey);
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
import { Member } from "@app/channel/components/member";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { getChannelUsers } from "@libs/storage";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { Member } from '@app/channel/components/member';
|
||||||
|
|
||||||
|
import { getChannelUsers } from '@libs/storage';
|
||||||
|
|
||||||
export function ChannelMembers({ id }: { id: string }) {
|
export function ChannelMembers({ id }: { id: string }) {
|
||||||
const { status, data, isFetching } = useQuery(
|
const { status, data, isFetching } = useQuery(['channel-members', id], async () => {
|
||||||
["channel-members", id],
|
|
||||||
async () => {
|
|
||||||
return await getChannelUsers(id);
|
return await getChannelUsers(id);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<h5 className="border-b border-zinc-900 pb-1 font-semibold text-zinc-200">
|
<h5 className="border-b border-zinc-900 pb-1 font-semibold text-zinc-200">
|
||||||
Members
|
Members
|
||||||
</h5>
|
</h5>
|
||||||
<div className="mt-3 w-full flex flex-wrap gap-1.5">
|
<div className="mt-3 flex w-full flex-wrap gap-1.5">
|
||||||
{status === "loading" || isFetching ? (
|
{status === 'loading' || isFetching ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
data.map((member: { pubkey: string }) => (
|
data.map((member: { pubkey: string }) => (
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { UserReply } from "@app/channel/components/messages/userReply";
|
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { useContext, useState } from 'react';
|
||||||
import { CancelIcon, EnterIcon } from "@shared/icons";
|
|
||||||
import { MediaUploader } from "@shared/mediaUploader";
|
import { UserReply } from '@app/channel/components/messages/userReply';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { useChannelMessages } from "@stores/channels";
|
import { CancelIcon, EnterIcon } from '@shared/icons';
|
||||||
import { dateToUnix } from "@utils/date";
|
import { MediaUploader } from '@shared/mediaUploader';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { useContext, useState } from "react";
|
|
||||||
|
import { useChannelMessages } from '@stores/channels';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/date';
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
|
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState('');
|
||||||
const [replyTo, closeReply] = useChannelMessages((state: any) => [
|
const [replyTo, closeReply] = useChannelMessages((state: any) => [
|
||||||
state.replyTo,
|
state.replyTo,
|
||||||
state.closeReply,
|
state.closeReply,
|
||||||
@@ -24,12 +28,12 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
|||||||
|
|
||||||
if (replyTo.id !== null) {
|
if (replyTo.id !== null) {
|
||||||
tags = [
|
tags = [
|
||||||
["e", channelID, "", "root"],
|
['e', channelID, '', 'root'],
|
||||||
["e", replyTo.id, "", "reply"],
|
['e', replyTo.id, '', 'reply'],
|
||||||
["p", replyTo.pubkey, ""],
|
['p', replyTo.pubkey, ''],
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
tags = [["e", channelID, "", "root"]];
|
tags = [['e', channelID, '', 'root']];
|
||||||
}
|
}
|
||||||
|
|
||||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||||
@@ -47,11 +51,11 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
|||||||
event.publish();
|
event.publish();
|
||||||
|
|
||||||
// reset state
|
// reset state
|
||||||
setValue("");
|
setValue('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnterPress = (e) => {
|
const handleEnterPress = (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
submit();
|
submit();
|
||||||
}
|
}
|
||||||
@@ -62,7 +66,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full ${replyTo.id ? "h-36" : "h-24"}`}>
|
<div className={`relative w-full ${replyTo.id ? 'h-36' : 'h-24'}`}>
|
||||||
{replyTo.id && (
|
{replyTo.id && (
|
||||||
<div className="absolute left-0 top-0 z-10 h-16 w-full p-[2px]">
|
<div className="absolute left-0 top-0 z-10 h-16 w-full p-[2px]">
|
||||||
<div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3">
|
<div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3">
|
||||||
@@ -89,11 +93,11 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder="Message"
|
placeholder="Message"
|
||||||
className={`relative ${
|
className={`relative ${
|
||||||
replyTo.id ? "h-36 pt-16" : "h-24 pt-3"
|
replyTo.id ? 'h-36 pt-16' : 'h-24 pt-3'
|
||||||
} w-full resize-none rounded-md px-5 !outline-none bg-zinc-800 placeholder:text-zinc-500`}
|
} w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500`}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-2 bottom-0 h-11">
|
<div className="absolute bottom-0 right-2 h-11">
|
||||||
<div className="h-full flex gap-3 items-center justify-end text-zinc-500">
|
<div className="flex h-full items-center justify-end gap-3 text-zinc-500">
|
||||||
<MediaUploader setState={setValue} />
|
<MediaUploader setState={setValue} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { CancelIcon, HideIcon } from "@shared/icons";
|
import { Fragment, useContext, useState } from 'react';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { Tooltip } from "@shared/tooltip_dep";
|
import { CancelIcon, HideIcon } from '@shared/icons';
|
||||||
import { useChannelMessages } from "@stores/channels";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { dateToUnix } from "@utils/date";
|
import { Tooltip } from '@shared/tooltip_dep';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
import { Fragment, useContext, useState } from "react";
|
import { useChannelMessages } from '@stores/channels';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/date';
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function MessageHideButton({ id }: { id: string }) {
|
export function MessageHideButton({ id }: { id: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -30,11 +33,11 @@ export function MessageHideButton({ id }: { id: string }) {
|
|||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
const event = new NDKEvent(ndk);
|
||||||
// build event
|
// build event
|
||||||
event.content = "";
|
event.content = '';
|
||||||
event.kind = 43;
|
event.kind = 43;
|
||||||
event.created_at = dateToUnix();
|
event.created_at = dateToUnix();
|
||||||
event.pubkey = account.pubkey;
|
event.pubkey = account.pubkey;
|
||||||
event.tags = [["e", id]];
|
event.tags = [['e', id]];
|
||||||
|
|
||||||
// publish event
|
// publish event
|
||||||
event.publish();
|
event.publish();
|
||||||
@@ -95,11 +98,7 @@ export function MessageHideButton({ id }: { id: string }) {
|
|||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<CancelIcon
|
<CancelIcon width={20} height={20} className="text-zinc-300" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description className="leading-tight text-zinc-400">
|
<Dialog.Description className="leading-tight text-zinc-400">
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { MessageHideButton } from "@app/channel/components/messages/hideButton";
|
import { MessageHideButton } from '@app/channel/components/messages/hideButton';
|
||||||
import { MessageMuteButton } from "@app/channel/components/messages/muteButton";
|
import { MessageMuteButton } from '@app/channel/components/messages/muteButton';
|
||||||
import { MessageReplyButton } from "@app/channel/components/messages/replyButton";
|
import { MessageReplyButton } from '@app/channel/components/messages/replyButton';
|
||||||
import { MentionNote } from "@shared/notes/mentions/note";
|
|
||||||
import { ImagePreview } from "@shared/notes/preview/image";
|
import { MentionNote } from '@shared/notes/mentions/note';
|
||||||
import { LinkPreview } from "@shared/notes/preview/link";
|
import { ImagePreview } from '@shared/notes/preview/image';
|
||||||
import { VideoPreview } from "@shared/notes/preview/video";
|
import { LinkPreview } from '@shared/notes/preview/link';
|
||||||
import { User } from "@shared/user";
|
import { VideoPreview } from '@shared/notes/preview/video';
|
||||||
import { parser } from "@utils/parser";
|
import { User } from '@shared/user';
|
||||||
import { LumeEvent } from "@utils/types";
|
|
||||||
|
import { parser } from '@utils/parser';
|
||||||
|
import { LumeEvent } from '@utils/types';
|
||||||
|
|
||||||
export function ChannelMessageItem({ data }: { data: LumeEvent }) {
|
export function ChannelMessageItem({ data }: { data: LumeEvent }) {
|
||||||
const content = parser(data);
|
const content = parser(data);
|
||||||
@@ -36,9 +38,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
{Array.isArray(content.notes) && content.notes.length ? (
|
{Array.isArray(content.notes) && content.notes.length ? (
|
||||||
content.notes.map((note: string) => (
|
content.notes.map((note: string) => <MentionNote key={note} id={note} />)
|
||||||
<MentionNote key={note} id={note} />
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
@@ -46,11 +46,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
|
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
|
||||||
<div className="inline-flex h-8 items-center justify-center gap-1.5 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
|
<div className="inline-flex h-8 items-center justify-center gap-1.5 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
|
||||||
<MessageReplyButton
|
<MessageReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
|
||||||
id={data.id}
|
|
||||||
pubkey={data.pubkey}
|
|
||||||
content={data.content}
|
|
||||||
/>
|
|
||||||
<MessageHideButton id={data.id} />
|
<MessageHideButton id={data.id} />
|
||||||
<MessageMuteButton pubkey={data.pubkey} />
|
<MessageMuteButton pubkey={data.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { CancelIcon, MuteIcon } from "@shared/icons";
|
import { Fragment, useContext, useState } from 'react';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { Tooltip } from "@shared/tooltip_dep";
|
import { CancelIcon, MuteIcon } from '@shared/icons';
|
||||||
import { useChannelMessages } from "@stores/channels";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { dateToUnix } from "@utils/date";
|
import { Tooltip } from '@shared/tooltip_dep';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
import { Fragment, useContext, useState } from "react";
|
import { useChannelMessages } from '@stores/channels';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/date';
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -30,11 +33,11 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
|||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
const event = new NDKEvent(ndk);
|
||||||
// build event
|
// build event
|
||||||
event.content = "";
|
event.content = '';
|
||||||
event.kind = 44;
|
event.kind = 44;
|
||||||
event.created_at = dateToUnix();
|
event.created_at = dateToUnix();
|
||||||
event.pubkey = account.pubkey;
|
event.pubkey = account.pubkey;
|
||||||
event.tags = [["p", pubkey]];
|
event.tags = [['p', pubkey]];
|
||||||
|
|
||||||
// publish event
|
// publish event
|
||||||
event.publish();
|
event.publish();
|
||||||
@@ -95,11 +98,7 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
|||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<CancelIcon
|
<CancelIcon width={20} height={20} className="text-zinc-300" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description className="leading-tight text-zinc-400">
|
<Dialog.Description className="leading-tight text-zinc-400">
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { ReplyMessageIcon } from "@shared/icons";
|
import { ReplyMessageIcon } from '@shared/icons';
|
||||||
import { Tooltip } from "@shared/tooltip_dep";
|
import { Tooltip } from '@shared/tooltip_dep';
|
||||||
import { useChannelMessages } from "@stores/channels";
|
|
||||||
|
import { useChannelMessages } from '@stores/channels';
|
||||||
|
|
||||||
export function MessageReplyButton({
|
export function MessageReplyButton({
|
||||||
id,
|
id,
|
||||||
pubkey,
|
pubkey,
|
||||||
content,
|
content,
|
||||||
}: { id: string; pubkey: string; content: string }) {
|
}: {
|
||||||
|
id: string;
|
||||||
|
pubkey: string;
|
||||||
|
content: string;
|
||||||
|
}) {
|
||||||
const openReply = useChannelMessages((state: any) => state.openReply);
|
const openReply = useChannelMessages((state: any) => state.openReply);
|
||||||
|
|
||||||
const createReply = () => {
|
const createReply = () => {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from '@shared/image';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
|
||||||
|
|
||||||
export function ChannelMessageUserMute({
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
pubkey,
|
|
||||||
}: {
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
pubkey: string;
|
|
||||||
}) {
|
export function ChannelMessageUserMute({ pubkey }: { pubkey: string }) {
|
||||||
const { user, isError, isLoading } = useProfile(pubkey);
|
const { user, isError, isLoading } = useProfile(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from '@shared/image';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function UserReply({ pubkey }: { pubkey: string }) {
|
export function UserReply({ pubkey }: { pubkey: string }) {
|
||||||
const { user, isError, isLoading } = useProfile(pubkey);
|
const { user, isError, isLoading } = useProfile(pubkey);
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
|
import { nip19 } from 'nostr-tools';
|
||||||
import { CopyIcon } from "@shared/icons";
|
|
||||||
import { Image } from "@shared/image";
|
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { nip19 } from "nostr-tools";
|
import { CopyIcon } from '@shared/icons';
|
||||||
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
export function ChannelMetadata({ id }: { id: string }) {
|
export function ChannelMetadata({ id }: { id: string }) {
|
||||||
const metadata = useChannelProfile(id);
|
const metadata = useChannelProfile(id);
|
||||||
const noteID = id ? nip19.noteEncode(id) : null;
|
const noteID = id ? nip19.noteEncode(id) : null;
|
||||||
|
|
||||||
const copyNoteID = async () => {
|
const copyNoteID = async () => {
|
||||||
const { writeText } = await import("@tauri-apps/api/clipboard");
|
const { writeText } = await import('@tauri-apps/api/clipboard');
|
||||||
if (noteID) {
|
if (noteID) {
|
||||||
await writeText(noteID);
|
await writeText(noteID);
|
||||||
}
|
}
|
||||||
@@ -17,19 +20,17 @@ export function ChannelMetadata({ id }: { id: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="relative shrink-0 rounded-md h-11 w-11">
|
<div className="relative h-11 w-11 shrink-0 rounded-md">
|
||||||
<Image
|
<Image
|
||||||
src={metadata?.picture}
|
src={metadata?.picture}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt={id}
|
alt={id}
|
||||||
className="h-11 w-11 rounded-md object-contain bg-zinc-900"
|
className="h-11 w-11 rounded-md bg-zinc-900 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="inline-flex items-center gap-1">
|
<div className="inline-flex items-center gap-1">
|
||||||
<h5 className="leading-none text-lg font-semibold">
|
<h5 className="text-lg font-semibold leading-none">{metadata?.name}</h5>
|
||||||
{metadata?.name}
|
|
||||||
</h5>
|
|
||||||
<button type="button" onClick={() => copyNoteID()}>
|
<button type="button" onClick={() => copyNoteID()}>
|
||||||
<CopyIcon width={14} height={14} className="text-zinc-400" />
|
<CopyIcon width={14} height={14} className="text-zinc-400" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { Image } from "@shared/image";
|
import { useState } from 'react';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { Image } from '@shared/image';
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
|
||||||
import { useState } from "react";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function MutedItem({ data }: { data: any }) {
|
export function MutedItem({ data }: { data: any }) {
|
||||||
const { user, isError, isLoading } = useProfile(data.content);
|
const { user, isError, isLoading } = useProfile(data.content);
|
||||||
const [status, setStatus] = useState(data.status);
|
const [status, setStatus] = useState(data.status);
|
||||||
|
|
||||||
const unmute = async () => {
|
const unmute = async () => {
|
||||||
const { updateItemInBlacklist } = await import("@libs/storage");
|
const { updateItemInBlacklist } = await import('@libs/storage');
|
||||||
const res = await updateItemInBlacklist(data.content, 0);
|
const res = await updateItemInBlacklist(data.content, 0);
|
||||||
if (res) {
|
if (res) {
|
||||||
setStatus(0);
|
setStatus(0);
|
||||||
@@ -17,7 +20,7 @@ export function MutedItem({ data }: { data: any }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mute = async () => {
|
const mute = async () => {
|
||||||
const { updateItemInBlacklist } = await import("@libs/storage");
|
const { updateItemInBlacklist } = await import('@libs/storage');
|
||||||
const res = await updateItemInBlacklist(data.content, 1);
|
const res = await updateItemInBlacklist(data.content, 1);
|
||||||
if (res) {
|
if (res) {
|
||||||
setStatus(1);
|
setStatus(1);
|
||||||
@@ -49,7 +52,7 @@ export function MutedItem({ data }: { data: any }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
|
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
|
||||||
<span className="truncate text-base font-medium leading-none text-zinc-100">
|
<span className="truncate text-base font-medium leading-none text-zinc-100">
|
||||||
{user?.displayName || user?.name || "Pleb"}
|
{user?.displayName || user?.name || 'Pleb'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-base leading-none text-zinc-400">
|
<span className="text-base leading-none text-zinc-400">
|
||||||
{shortenKey(data.content)}
|
{shortenKey(data.content)}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { getChannel, updateChannelMetadata } from "@libs/storage";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { useContext, useEffect } from 'react';
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useContext, useEffect } from "react";
|
import { getChannel, updateChannelMetadata } from '@libs/storage';
|
||||||
|
|
||||||
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
|
|
||||||
export function useChannelProfile(id: string) {
|
export function useChannelProfile(id: string) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const { data } = useQuery(["channel-metadata", id], async () => {
|
const { data } = useQuery(['channel-metadata', id], async () => {
|
||||||
return await getChannel(id);
|
return await getChannel(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -13,15 +15,15 @@ export function useChannelProfile(id: string) {
|
|||||||
// subscribe to channel
|
// subscribe to channel
|
||||||
const sub = ndk.subscribe(
|
const sub = ndk.subscribe(
|
||||||
{
|
{
|
||||||
"#e": [id],
|
'#e': [id],
|
||||||
kinds: [41],
|
kinds: [41],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
closeOnEose: true,
|
closeOnEose: true,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
sub.addListener("event", (event: { content: string }) => {
|
sub.addListener('event', (event: { content: string }) => {
|
||||||
// update in local database
|
// update in local database
|
||||||
updateChannelMetadata(id, event.content);
|
updateChannelMetadata(id, event.content);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { ChannelMessageItem } from "./components/messages/item";
|
import { useCallback, useContext, useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
import { ChannelMembers } from "@app/channel/components/members";
|
import { useParams } from 'react-router-dom';
|
||||||
import { ChannelMessageForm } from "@app/channel/components/messages/form";
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { ChannelMetadata } from "@app/channel/components/metadata";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { ChannelMembers } from '@app/channel/components/members';
|
||||||
import { useChannelMessages } from "@stores/channels";
|
import { ChannelMessageForm } from '@app/channel/components/messages/form';
|
||||||
import { dateToUnix, getHourAgo } from "@utils/date";
|
import { ChannelMetadata } from '@app/channel/components/metadata';
|
||||||
import { LumeEvent } from "@utils/types";
|
|
||||||
import {
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
useCallback,
|
|
||||||
useContext,
|
import { useChannelMessages } from '@stores/channels';
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
import { dateToUnix, getHourAgo } from '@utils/date';
|
||||||
useRef,
|
import { LumeEvent } from '@utils/types';
|
||||||
} from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { ChannelMessageItem } from './components/messages/item';
|
||||||
import { Virtuoso } from "react-virtuoso";
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
@@ -25,11 +24,11 @@ const Header = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
|
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
|
||||||
{getHourAgo(24, now).toLocaleDateString("en-US", {
|
{getHourAgo(24, now).toLocaleDateString('en-US', {
|
||||||
weekday: "long",
|
weekday: 'long',
|
||||||
year: "numeric",
|
year: 'numeric',
|
||||||
month: "long",
|
month: 'long',
|
||||||
day: "numeric",
|
day: 'numeric',
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,13 +52,9 @@ export function ChannelScreen() {
|
|||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
const [messages, fetchMessages, addMessage, clearMessages] =
|
const [messages, fetchMessages, addMessage, clearMessages] = useChannelMessages(
|
||||||
useChannelMessages((state: any) => [
|
(state: any) => [state.messages, state.fetch, state.add, state.clear]
|
||||||
state.messages,
|
);
|
||||||
state.fetch,
|
|
||||||
state.add,
|
|
||||||
state.clear,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
fetchMessages(id);
|
fetchMessages(id);
|
||||||
@@ -69,14 +64,14 @@ export function ChannelScreen() {
|
|||||||
// subscribe to channel
|
// subscribe to channel
|
||||||
const sub = ndk.subscribe(
|
const sub = ndk.subscribe(
|
||||||
{
|
{
|
||||||
"#e": [id],
|
'#e': [id],
|
||||||
kinds: [42],
|
kinds: [42],
|
||||||
since: dateToUnix(),
|
since: dateToUnix(),
|
||||||
},
|
},
|
||||||
{ closeOnEose: false },
|
{ closeOnEose: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
sub.addListener("event", (event: LumeEvent) => {
|
sub.addListener('event', (event: LumeEvent) => {
|
||||||
addMessage(id, event);
|
addMessage(id, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,28 +85,28 @@ export function ChannelScreen() {
|
|||||||
(index: string | number) => {
|
(index: string | number) => {
|
||||||
return <ChannelMessageItem data={messages[index]} />;
|
return <ChannelMessageItem data={messages[index]} />;
|
||||||
},
|
},
|
||||||
[messages],
|
[messages]
|
||||||
);
|
);
|
||||||
|
|
||||||
const computeItemKey = useCallback(
|
const computeItemKey = useCallback(
|
||||||
(index: string | number) => {
|
(index: string | number) => {
|
||||||
return messages[index].event_id;
|
return messages[index].event_id;
|
||||||
},
|
},
|
||||||
[messages],
|
[messages]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full grid grid-cols-3">
|
<div className="grid h-full w-full grid-cols-3">
|
||||||
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
|
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
|
||||||
>
|
>
|
||||||
<h3 className="font-semibold text-zinc-100">Public Channel</h3>
|
<h3 className="font-semibold text-zinc-100">Public Channel</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full flex-1 p-3">
|
<div className="h-full w-full flex-1 p-3">
|
||||||
<div className="h-full flex flex-col justify-between rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
<div className="flex h-full flex-col justify-between overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="flex-1 w-full h-full">
|
<div className="h-full w-full flex-1">
|
||||||
{!messages ? (
|
{!messages ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -133,7 +128,7 @@ export function ChannelScreen() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 px-5 p-3 rounded-b-xl border-t border-zinc-800 bg-zinc-900 z-50">
|
<div className="z-50 shrink-0 rounded-b-xl border-t border-zinc-800 bg-zinc-900 p-3 px-5">
|
||||||
<ChannelMessageForm channelID={id} />
|
<ChannelMessageForm channelID={id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,9 +137,9 @@ export function ChannelScreen() {
|
|||||||
<div className="col-span-1 flex flex-col">
|
<div className="col-span-1 flex flex-col">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
|
||||||
/>
|
/>
|
||||||
<div className="p-3 flex flex-col gap-3">
|
<div className="flex flex-col gap-3 p-3">
|
||||||
<ChannelMetadata id={id} />
|
<ChannelMetadata id={id} />
|
||||||
<ChannelMembers id={id} />
|
<ChannelMembers id={id} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { Image } from "@shared/image";
|
import { NavLink } from 'react-router-dom';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { Image } from '@shared/image';
|
||||||
import { NavLink } from "react-router-dom";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function ChatsListItem({ data }: { data: any }) {
|
export function ChatsListItem({ data }: { data: any }) {
|
||||||
const { status, user } = useProfile(data.sender_pubkey);
|
const { status, user } = useProfile(data.sender_pubkey);
|
||||||
|
|
||||||
if (status === "loading") {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
@@ -23,12 +26,12 @@ export function ChatsListItem({ data }: { data: any }) {
|
|||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
|
||||||
isActive ? "bg-zinc-900/50 text-zinc-100" : "",
|
isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<Image
|
<Image
|
||||||
src={user?.image}
|
src={user?.image}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
@@ -36,7 +39,7 @@ export function ChatsListItem({ data }: { data: any }) {
|
|||||||
className="h-6 w-6 rounded object-cover"
|
className="h-6 w-6 rounded object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full inline-flex items-center justify-between">
|
<div className="inline-flex w-full items-center justify-between">
|
||||||
<div className="inline-flex items-baseline gap-1">
|
<div className="inline-flex items-baseline gap-1">
|
||||||
<h5 className="max-w-[10rem] truncate font-medium text-zinc-200">
|
<h5 className="max-w-[10rem] truncate font-medium text-zinc-200">
|
||||||
{user?.nip05 ||
|
{user?.nip05 ||
|
||||||
@@ -47,7 +50,7 @@ export function ChatsListItem({ data }: { data: any }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{data.new_messages > 0 && (
|
{data.new_messages > 0 && (
|
||||||
<span className="inline-flex items-center justify-center rounded bg-fuchsia-400/10 w-8 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">
|
<span className="inline-flex w-8 items-center justify-center rounded bg-fuchsia-400/10 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">
|
||||||
{data.new_messages}
|
{data.new_messages}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ChatsListItem } from "@app/chat/components/item";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { NewMessageModal } from "@app/chat/components/modal";
|
|
||||||
import { ChatsListSelfItem } from "@app/chat/components/self";
|
import { ChatsListItem } from '@app/chat/components/item';
|
||||||
import { getChatsByPubkey } from "@libs/storage";
|
import { NewMessageModal } from '@app/chat/components/modal';
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { ChatsListSelfItem } from '@app/chat/components/self';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
|
import { getChatsByPubkey } from '@libs/storage';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function ChatsList() {
|
export function ChatsList() {
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
@@ -13,29 +16,29 @@ export function ChatsList() {
|
|||||||
data: chats,
|
data: chats,
|
||||||
isFetching,
|
isFetching,
|
||||||
} = useQuery(
|
} = useQuery(
|
||||||
["chats"],
|
['chats'],
|
||||||
async () => {
|
async () => {
|
||||||
const chats = await getChatsByPubkey(account.pubkey);
|
const chats = await getChatsByPubkey(account.pubkey);
|
||||||
const sorted = chats.sort(
|
const sorted = chats.sort(
|
||||||
(a, b) => parseInt(a.new_messages) - parseInt(b.new_messages),
|
(a, b) => parseInt(a.new_messages) - parseInt(b.new_messages)
|
||||||
);
|
);
|
||||||
return sorted;
|
return sorted;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: account ? true : false,
|
enabled: account ? true : false,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status === "loading") {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -49,7 +52,7 @@ export function ChatsList() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{chats.map((item) => {
|
{chats.map((item) => {
|
||||||
@@ -60,7 +63,7 @@ export function ChatsList() {
|
|||||||
{isFetching && (
|
{isFetching && (
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
|
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { usePublish } from "@libs/ndk";
|
import { nip04 } from 'nostr-tools';
|
||||||
import { EnterIcon } from "@shared/icons";
|
import { useCallback, useState } from 'react';
|
||||||
import { MediaUploader } from "@shared/mediaUploader";
|
|
||||||
import { nip04 } from "nostr-tools";
|
import { usePublish } from '@libs/ndk';
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
import { EnterIcon } from '@shared/icons';
|
||||||
|
import { MediaUploader } from '@shared/mediaUploader';
|
||||||
|
|
||||||
export function ChatMessageForm({
|
export function ChatMessageForm({
|
||||||
receiverPubkey,
|
receiverPubkey,
|
||||||
userPrivkey,
|
userPrivkey,
|
||||||
}: { receiverPubkey: string; userPubkey: string; userPrivkey: string }) {
|
}: {
|
||||||
|
receiverPubkey: string;
|
||||||
|
userPubkey: string;
|
||||||
|
userPrivkey: string;
|
||||||
|
}) {
|
||||||
const publish = usePublish();
|
const publish = usePublish();
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
const encryptMessage = useCallback(async () => {
|
const encryptMessage = useCallback(async () => {
|
||||||
return await nip04.encrypt(userPrivkey, receiverPubkey, value);
|
return await nip04.encrypt(userPrivkey, receiverPubkey, value);
|
||||||
@@ -17,13 +23,13 @@ export function ChatMessageForm({
|
|||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const message = await encryptMessage();
|
const message = await encryptMessage();
|
||||||
const tags = [["p", receiverPubkey]];
|
const tags = [['p', receiverPubkey]];
|
||||||
|
|
||||||
// publish message
|
// publish message
|
||||||
await publish({ content: message, kind: 4, tags });
|
await publish({ content: message, kind: 4, tags });
|
||||||
|
|
||||||
// reset state
|
// reset state
|
||||||
setValue("");
|
setValue('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnterPress = (e: {
|
const handleEnterPress = (e: {
|
||||||
@@ -31,7 +37,7 @@ export function ChatMessageForm({
|
|||||||
shiftKey: any;
|
shiftKey: any;
|
||||||
preventDefault: () => void;
|
preventDefault: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
submit();
|
submit();
|
||||||
}
|
}
|
||||||
@@ -45,10 +51,10 @@ export function ChatMessageForm({
|
|||||||
onKeyDown={handleEnterPress}
|
onKeyDown={handleEnterPress}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder="Message"
|
placeholder="Message"
|
||||||
className="relative h-11 w-full resize-none rounded-md px-5 !outline-none bg-zinc-800 placeholder:text-zinc-500"
|
className="relative h-11 w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-2 top-0 h-11">
|
<div className="absolute right-2 top-0 h-11">
|
||||||
<div className="h-full flex gap-3 items-center justify-end text-zinc-500">
|
<div className="flex h-full items-center justify-end gap-3 text-zinc-500">
|
||||||
<MediaUploader setState={setValue} />
|
<MediaUploader setState={setValue} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useDecryptMessage } from "@app/chat/hooks/useDecryptMessage";
|
import { useDecryptMessage } from '@app/chat/hooks/useDecryptMessage';
|
||||||
import { MentionNote } from "@shared/notes/mentions/note";
|
|
||||||
import { ImagePreview } from "@shared/notes/preview/image";
|
import { MentionNote } from '@shared/notes/mentions/note';
|
||||||
import { LinkPreview } from "@shared/notes/preview/link";
|
import { ImagePreview } from '@shared/notes/preview/image';
|
||||||
import { VideoPreview } from "@shared/notes/preview/video";
|
import { LinkPreview } from '@shared/notes/preview/link';
|
||||||
import { User } from "@shared/user";
|
import { VideoPreview } from '@shared/notes/preview/video';
|
||||||
import { parser } from "@utils/parser";
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
|
import { parser } from '@utils/parser';
|
||||||
|
|
||||||
export function ChatMessageItem({
|
export function ChatMessageItem({
|
||||||
data,
|
data,
|
||||||
@@ -18,7 +20,7 @@ export function ChatMessageItem({
|
|||||||
const decryptedContent = useDecryptMessage(data, userPubkey, userPrivkey);
|
const decryptedContent = useDecryptMessage(data, userPubkey, userPrivkey);
|
||||||
// if we have decrypted content, use it instead of the encrypted content
|
// if we have decrypted content, use it instead of the encrypted content
|
||||||
if (decryptedContent) {
|
if (decryptedContent) {
|
||||||
data["content"] = decryptedContent;
|
data['content'] = decryptedContent;
|
||||||
}
|
}
|
||||||
// parse the note content
|
// parse the note content
|
||||||
const content = parser(data);
|
const content = parser(data);
|
||||||
@@ -26,11 +28,7 @@ export function ChatMessageItem({
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<User
|
<User pubkey={data.sender_pubkey} time={data.created_at} isChat={true} />
|
||||||
pubkey={data.sender_pubkey}
|
|
||||||
time={data.created_at}
|
|
||||||
isChat={true}
|
|
||||||
/>
|
|
||||||
<div className="-mt-[20px] pl-[49px]">
|
<div className="-mt-[20px] pl-[49px]">
|
||||||
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||||
{content.parsed}
|
{content.parsed}
|
||||||
@@ -39,9 +37,7 @@ export function ChatMessageItem({
|
|||||||
{content.videos.length > 0 && <VideoPreview urls={content.videos} />}
|
{content.videos.length > 0 && <VideoPreview urls={content.videos} />}
|
||||||
{content.links.length > 0 && <LinkPreview urls={content.links} />}
|
{content.links.length > 0 && <LinkPreview urls={content.links} />}
|
||||||
{content.notes.length > 0 &&
|
{content.notes.length > 0 &&
|
||||||
content.notes.map((note: string) => (
|
content.notes.map((note: string) => <MentionNote key={note} id={note} />)}
|
||||||
<MentionNote key={note} id={note} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { User } from "@app/auth/components/user";
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Fragment, useState } from 'react';
|
||||||
import { CancelIcon, LoaderIcon, PlusIcon } from "@shared/icons";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
import { Fragment, useState } from "react";
|
import { User } from '@app/auth/components/user';
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
import { CancelIcon, LoaderIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function NewMessageModal() {
|
export function NewMessageModal() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -77,22 +80,17 @@ export function NewMessageModal() {
|
|||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<CancelIcon
|
<CancelIcon width={20} height={20} className="text-zinc-300" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
||||||
All messages will be encrypted, but anyone can see who you
|
All messages will be encrypted, but anyone can see who you chat
|
||||||
chat
|
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[500px] flex flex-col pb-5 overflow-x-hidden overflow-y-auto">
|
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-5">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-4 py-3 inline-flex items-center justify-center">
|
<div className="inline-flex items-center justify-center px-4 py-3">
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -106,7 +104,7 @@ export function NewMessageModal() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openChat(follow)}
|
onClick={() => openChat(follow)}
|
||||||
className="inline-flex text-sm w-max px-3 py-1.5 rounded border-t border-zinc-600/50 bg-zinc-700 hover:bg-fuchsia-500 transform translate-x-20 group-hover:translate-x-0 transition-transform ease-in-out duration-150"
|
className="inline-flex w-max translate-x-20 transform rounded border-t border-zinc-600/50 bg-zinc-700 px-3 py-1.5 text-sm transition-transform duration-150 ease-in-out hover:bg-fuchsia-500 group-hover:translate-x-0"
|
||||||
>
|
>
|
||||||
Chat
|
Chat
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { Image } from "@shared/image";
|
import { NavLink } from 'react-router-dom';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { Image } from '@shared/image';
|
||||||
import { NavLink } from "react-router-dom";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function ChatsListSelfItem({ data }: { data: any }) {
|
export function ChatsListSelfItem({ data }: { data: any }) {
|
||||||
const { status, user } = useProfile(data.pubkey);
|
const { status, user } = useProfile(data.pubkey);
|
||||||
|
|
||||||
if (status === "loading") {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||||
@@ -25,8 +28,8 @@ export function ChatsListSelfItem({ data }: { data: any }) {
|
|||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
|
||||||
isActive ? "bg-zinc-900/50 text-zinc-100" : "",
|
isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Link } from 'react-router-dom';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { Image } from '@shared/image';
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
|
||||||
import { Link } from "react-router-dom";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function ChatSidebar({ pubkey }: { pubkey: string }) {
|
export function ChatSidebar({ pubkey }: { pubkey: string }) {
|
||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
@@ -20,7 +23,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h3 className="leading-none text-lg font-semibold">
|
<h3 className="text-lg font-semibold leading-none">
|
||||||
{user?.displayName || user?.name}
|
{user?.displayName || user?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<h5 className="leading-none text-zinc-400">
|
<h5 className="leading-none text-zinc-400">
|
||||||
@@ -31,7 +34,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
|
|||||||
<p className="leading-tight">{user?.bio || user?.about}</p>
|
<p className="leading-tight">{user?.bio || user?.about}</p>
|
||||||
<Link
|
<Link
|
||||||
to={`/app/user/${pubkey}`}
|
to={`/app/user/${pubkey}`}
|
||||||
className="mt-3 inline-flex w-full h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-zinc-800 text-sm text-zinc-300 hover:text-zinc-100 font-medium"
|
className="mt-3 inline-flex h-10 w-full items-center justify-center rounded-md bg-zinc-900 text-sm font-medium text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100"
|
||||||
>
|
>
|
||||||
View full profile
|
View full profile
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { nip04 } from "nostr-tools";
|
import { nip04 } from 'nostr-tools';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function useDecryptMessage(
|
export function useDecryptMessage(data: any, userPubkey: string, userPriv: string) {
|
||||||
data: any,
|
|
||||||
userPubkey: string,
|
|
||||||
userPriv: string,
|
|
||||||
) {
|
|
||||||
const [content, setContent] = useState(data.content);
|
const [content, setContent] = useState(data.content);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function decrypt() {
|
async function decrypt() {
|
||||||
const pubkey =
|
const pubkey =
|
||||||
userPubkey === data.sender_pubkey
|
userPubkey === data.sender_pubkey ? data.receiver_pubkey : data.sender_pubkey;
|
||||||
? data.receiver_pubkey
|
|
||||||
: data.sender_pubkey;
|
|
||||||
const result = await nip04.decrypt(userPriv, pubkey, data.content);
|
const result = await nip04.decrypt(userPriv, pubkey, data.content);
|
||||||
setContent(result);
|
setContent(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { ChatMessageForm } from "@app/chat/components/messages/form";
|
import { NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||||
import { ChatMessageItem } from "@app/chat/components/messages/item";
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ChatSidebar } from "@app/chat/components/sidebar";
|
import { useCallback, useContext, useEffect, useRef } from 'react';
|
||||||
import { createChat, getChatMessages } from "@libs/storage";
|
import { useParams } from 'react-router-dom';
|
||||||
import { NDKSubscription } from "@nostr-dev-kit/ndk";
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { ChatMessageForm } from '@app/chat/components/messages/form';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { ChatMessageItem } from '@app/chat/components/messages/item';
|
||||||
import { useCallback, useContext, useEffect, useRef } from "react";
|
import { ChatSidebar } from '@app/chat/components/sidebar';
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { createChat, getChatMessages } from '@libs/storage';
|
||||||
|
|
||||||
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function ChatScreen() {
|
export function ChatScreen() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -18,13 +22,13 @@ export function ChatScreen() {
|
|||||||
const { pubkey } = useParams();
|
const { pubkey } = useParams();
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
const { status, data } = useQuery(
|
const { status, data } = useQuery(
|
||||||
["chat", pubkey],
|
['chat', pubkey],
|
||||||
async () => {
|
async () => {
|
||||||
return await getChatMessages(account.pubkey, pubkey);
|
return await getChatMessages(account.pubkey, pubkey);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: account ? true : false,
|
enabled: account ? true : false,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemContent: any = useCallback(
|
const itemContent: any = useCallback(
|
||||||
@@ -37,14 +41,14 @@ export function ChatScreen() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[data],
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
const computeItemKey = useCallback(
|
const computeItemKey = useCallback(
|
||||||
(index: string | number) => {
|
(index: string | number) => {
|
||||||
return data[index].id;
|
return data[index].id;
|
||||||
},
|
},
|
||||||
[data],
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
const chat = useMutation({
|
const chat = useMutation({
|
||||||
@@ -55,11 +59,11 @@ export function ChatScreen() {
|
|||||||
data.sender_pubkey,
|
data.sender_pubkey,
|
||||||
data.content,
|
data.content,
|
||||||
data.tags,
|
data.tags,
|
||||||
data.created_at,
|
data.created_at
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["chat", pubkey] });
|
queryClient.invalidateQueries({ queryKey: ['chat', pubkey] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,15 +72,15 @@ export function ChatScreen() {
|
|||||||
{
|
{
|
||||||
kinds: [4],
|
kinds: [4],
|
||||||
authors: [account.pubkey],
|
authors: [account.pubkey],
|
||||||
"#p": [pubkey],
|
'#p': [pubkey],
|
||||||
since: Math.floor(Date.now() / 1000),
|
since: Math.floor(Date.now() / 1000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
closeOnEose: false,
|
closeOnEose: false,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
sub.addListener("event", (event) => {
|
sub.addListener('event', (event) => {
|
||||||
chat.mutate({
|
chat.mutate({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
receiver_pubkey: pubkey,
|
receiver_pubkey: pubkey,
|
||||||
@@ -93,18 +97,18 @@ export function ChatScreen() {
|
|||||||
}, [pubkey]);
|
}, [pubkey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full grid grid-cols-3">
|
<div className="grid h-full w-full grid-cols-3">
|
||||||
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
|
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
|
||||||
>
|
>
|
||||||
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
|
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full flex-1 p-3">
|
<div className="h-full w-full flex-1 p-3">
|
||||||
<div className="h-full flex flex-col justify-between rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
<div className="flex h-full flex-col justify-between overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="flex-1 w-full h-full">
|
<div className="h-full w-full flex-1">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
@@ -117,14 +121,14 @@ export function ChatScreen() {
|
|||||||
followOutput={true}
|
followOutput={true}
|
||||||
overscan={50}
|
overscan={50}
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||||
className="relative scrollbar-hide overflow-y-auto"
|
className="scrollbar-hide relative overflow-y-auto"
|
||||||
components={{
|
components={{
|
||||||
EmptyPlaceholder: () => Empty,
|
EmptyPlaceholder: () => Empty,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 px-5 p-3 rounded-b-xl border-t border-zinc-800 bg-zinc-900 z-50">
|
<div className="z-50 shrink-0 rounded-b-xl border-t border-zinc-800 bg-zinc-900 p-3 px-5">
|
||||||
<ChatMessageForm
|
<ChatMessageForm
|
||||||
receiverPubkey={pubkey}
|
receiverPubkey={pubkey}
|
||||||
userPubkey={account.pubkey}
|
userPubkey={account.pubkey}
|
||||||
@@ -137,7 +141,7 @@ export function ChatScreen() {
|
|||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
|
||||||
/>
|
/>
|
||||||
<ChatSidebar pubkey={pubkey} />
|
<ChatSidebar pubkey={pubkey} />
|
||||||
</div>
|
</div>
|
||||||
@@ -146,10 +150,10 @@ export function ChatScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Empty = (
|
const Empty = (
|
||||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full flex flex-col gap-1 text-center">
|
<div className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 transform flex-col gap-1 text-center">
|
||||||
<h3 className="mb-2 text-4xl">🙌</h3>
|
<h3 className="mb-2 text-4xl">🙌</h3>
|
||||||
<p className="leading-none text-zinc-400">
|
<p className="leading-none text-zinc-400">
|
||||||
You two didn't talk yet, let's send first message
|
You two didn't talk yet, let's send first message
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useRouteError } from "react-router-dom";
|
import { useRouteError } from 'react-router-dom';
|
||||||
|
|
||||||
export function ErrorScreen() {
|
export function ErrorScreen() {
|
||||||
const error: any = useRouteError();
|
const error: any = useRouteError();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div>
|
<div>
|
||||||
<h1>Oops!</h1>
|
<h1>Oops!</h1>
|
||||||
<p>Sorry, an unexpected error has occurred.</p>
|
<p>Sorry, an unexpected error has occurred.</p>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { prefetchEvents } from "@libs/ndk";
|
import { NDKFilter } from '@nostr-dev-kit/ndk';
|
||||||
|
import { useContext, useEffect, useRef } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { prefetchEvents } from '@libs/ndk';
|
||||||
import {
|
import {
|
||||||
countTotalNotes,
|
countTotalNotes,
|
||||||
createChannelMessage,
|
createChannelMessage,
|
||||||
@@ -7,14 +11,13 @@ import {
|
|||||||
getChannels,
|
getChannels,
|
||||||
getLastLogin,
|
getLastLogin,
|
||||||
updateLastLogin,
|
updateLastLogin,
|
||||||
} from "@libs/storage";
|
} from '@libs/storage';
|
||||||
import { NDKFilter } from "@nostr-dev-kit/ndk";
|
|
||||||
import { LoaderIcon, LumeIcon } from "@shared/icons";
|
import { LoaderIcon, LumeIcon } from '@shared/icons';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { dateToUnix, getHourAgo } from "@utils/date";
|
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { dateToUnix, getHourAgo } from '@utils/date';
|
||||||
import { useContext, useEffect, useRef } from "react";
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
const totalNotes = await countTotalNotes();
|
const totalNotes = await countTotalNotes();
|
||||||
const lastLogin = await getLastLogin();
|
const lastLogin = await getLastLogin();
|
||||||
@@ -51,13 +54,13 @@ export function Root() {
|
|||||||
event.kind,
|
event.kind,
|
||||||
event.tags,
|
event.tags,
|
||||||
event.content,
|
event.content,
|
||||||
event.created_at,
|
event.created_at
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("error: ", e);
|
console.log('error: ', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +73,7 @@ export function Root() {
|
|||||||
};
|
};
|
||||||
const receiveFilter: NDKFilter = {
|
const receiveFilter: NDKFilter = {
|
||||||
kinds: [4],
|
kinds: [4],
|
||||||
"#p": [account.pubkey],
|
'#p': [account.pubkey],
|
||||||
since: lastLogin,
|
since: lastLogin,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,24 +82,24 @@ export function Root() {
|
|||||||
const events = [...sendMessages, ...receiveMessages];
|
const events = [...sendMessages, ...receiveMessages];
|
||||||
|
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
const receiverPubkey =
|
const receiverPubkey = event.tags.find((t) => t[0] === 'p')[1] || account.pubkey;
|
||||||
event.tags.find((t) => t[0] === "p")[1] || account.pubkey;
|
|
||||||
createChat(
|
createChat(
|
||||||
event.id,
|
event.id,
|
||||||
receiverPubkey,
|
receiverPubkey,
|
||||||
event.pubkey,
|
event.pubkey,
|
||||||
event.content,
|
event.content,
|
||||||
event.tags,
|
event.tags,
|
||||||
event.created_at,
|
event.created_at
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("error: ", e);
|
console.log('error: ', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
async function fetchChannelMessages() {
|
async function fetchChannelMessages() {
|
||||||
try {
|
try {
|
||||||
const ids = [];
|
const ids = [];
|
||||||
@@ -105,11 +108,10 @@ export function Root() {
|
|||||||
ids.push(channel.event_id);
|
ids.push(channel.event_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const since =
|
const since = lastLogin === 0 ? dateToUnix(getHourAgo(48, now.current)) : lastLogin;
|
||||||
lastLogin === 0 ? dateToUnix(getHourAgo(48, now.current)) : lastLogin;
|
|
||||||
|
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
"#e": ids,
|
'#e': ids,
|
||||||
kinds: [42],
|
kinds: [42],
|
||||||
since: since,
|
since: since,
|
||||||
};
|
};
|
||||||
@@ -125,16 +127,17 @@ export function Root() {
|
|||||||
event.kind,
|
event.kind,
|
||||||
event.content,
|
event.content,
|
||||||
event.tags,
|
event.tags,
|
||||||
event.created_at,
|
event.created_at
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("error: ", e);
|
console.log('error: ', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function prefetch() {
|
async function prefetch() {
|
||||||
@@ -145,12 +148,12 @@ export function Root() {
|
|||||||
if (chats) {
|
if (chats) {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
await updateLastLogin(now);
|
await updateLastLogin(now);
|
||||||
navigate("/app/space", { replace: true });
|
navigate('/app/space', { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "success" && account) {
|
if (status === 'success' && account) {
|
||||||
prefetch();
|
prefetch();
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [status]);
|
||||||
@@ -170,8 +173,7 @@ export function Root() {
|
|||||||
Here's an interesting fact:
|
Here's an interesting fact:
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-medium text-zinc-300 dark:text-zinc-600">
|
<p className="font-medium text-zinc-300 dark:text-zinc-600">
|
||||||
Bitcoin and Nostr can be used by anyone, and no one can stop
|
Bitcoin and Nostr can be used by anyone, and no one can stop you!
|
||||||
you!
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,50 +1,55 @@
|
|||||||
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
|
import { useState } from 'react';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
import { useState } from "react";
|
import { EyeOffIcon, EyeOnIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function AccountSettingsScreen() {
|
export function AccountSettingsScreen() {
|
||||||
const { status, account } = useAccount();
|
const { status, account } = useAccount();
|
||||||
const [type, setType] = useState("password");
|
const [type, setType] = useState('password');
|
||||||
|
|
||||||
const showPrivateKey = () => {
|
const showPrivateKey = () => {
|
||||||
if (type === "password") {
|
if (type === 'password') {
|
||||||
setType("text");
|
setType('text');
|
||||||
} else {
|
} else {
|
||||||
setType("password");
|
setType('password');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full px-3 pt-12">
|
<div className="h-full w-full px-3 pt-12">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-semibold text-zinc-100">Account</h1>
|
<h1 className="text-lg font-semibold text-zinc-100">Account</h1>
|
||||||
<div className="">
|
<div className="">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-base font-semibold text-zinc-400">
|
<label htmlFor="pubkey" className="text-base font-semibold text-zinc-400">
|
||||||
Public Key
|
Public Key
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
value={account.pubkey}
|
value={account.pubkey}
|
||||||
className="relative w-2/3 rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
className="relative w-2/3 rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-base font-semibold text-zinc-400">
|
<label htmlFor="npub" className="text-base font-semibold text-zinc-400">
|
||||||
Npub
|
Npub
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
value={account.npub}
|
value={account.npub}
|
||||||
className="relative w-2/3 rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
className="relative w-2/3 rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-base font-semibold text-zinc-400">
|
<label
|
||||||
|
htmlFor="privkey"
|
||||||
|
className="text-base font-semibold text-zinc-400"
|
||||||
|
>
|
||||||
Private Key
|
Private Key
|
||||||
</label>
|
</label>
|
||||||
<div className="relative w-2/3">
|
<div className="relative w-2/3">
|
||||||
@@ -52,14 +57,14 @@ export function AccountSettingsScreen() {
|
|||||||
readOnly
|
readOnly
|
||||||
type={type}
|
type={type}
|
||||||
value={account.privkey}
|
value={account.privkey}
|
||||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showPrivateKey()}
|
onClick={() => showPrivateKey()}
|
||||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
{type === "password" ? (
|
{type === 'password' ? (
|
||||||
<EyeOffIcon
|
<EyeOffIcon
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from '@headlessui/react';
|
||||||
import { getSetting, updateSetting } from "@libs/storage";
|
import { useEffect, useState } from 'react';
|
||||||
import { useEffect, useState } from "react";
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { twMerge } from "tailwind-merge";
|
import { disable, enable, isEnabled } from 'tauri-plugin-autostart-api';
|
||||||
import { disable, enable, isEnabled } from "tauri-plugin-autostart-api";
|
|
||||||
|
import { getSetting, updateSetting } from '@libs/storage';
|
||||||
|
|
||||||
export function AutoStartSetting() {
|
export function AutoStartSetting() {
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
@@ -10,18 +11,18 @@ export function AutoStartSetting() {
|
|||||||
const toggle = async () => {
|
const toggle = async () => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
await enable();
|
await enable();
|
||||||
await updateSetting("auto_start", 1);
|
await updateSetting('auto_start', 1);
|
||||||
console.log(`registered for autostart? ${await isEnabled()}`);
|
console.log(`registered for autostart? ${await isEnabled()}`);
|
||||||
} else {
|
} else {
|
||||||
await disable();
|
await disable();
|
||||||
await updateSetting("auto_start", 0);
|
await updateSetting('auto_start', 0);
|
||||||
}
|
}
|
||||||
setEnabled(!enabled);
|
setEnabled(!enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getAppSetting() {
|
async function getAppSetting() {
|
||||||
const setting = await getSetting("auto_start");
|
const setting = await getSetting('auto_start');
|
||||||
if (parseInt(setting) === 0) {
|
if (parseInt(setting) === 0) {
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -32,27 +33,23 @@ export function AutoStartSetting() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-5 py-4 inline-flex items-center justify-between">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="leading-none font-medium text-zinc-200">
|
<span className="font-medium leading-none text-zinc-200">Auto start</span>
|
||||||
Auto start
|
<span className="text-sm leading-none text-zinc-400">Auto start at login</span>
|
||||||
</span>
|
|
||||||
<span className="leading-none text-sm text-zinc-400">
|
|
||||||
Auto start at login
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onChange={toggle}
|
onChange={toggle}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2",
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2',
|
||||||
enabled ? "bg-fuchsia-500" : "bg-zinc-700",
|
enabled ? 'bg-fuchsia-500' : 'bg-zinc-700'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-zinc-900 shadow ring-0 transition duration-200 ease-in-out",
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-zinc-900 shadow ring-0 transition duration-200 ease-in-out',
|
||||||
enabled ? "translate-x-5" : "translate-x-0",
|
enabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { getSetting, updateSetting } from "@libs/storage";
|
import { useState } from 'react';
|
||||||
import { CheckCircleIcon } from "@shared/icons";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const setting = await getSetting("cache_time");
|
import { getSetting, updateSetting } from '@libs/storage';
|
||||||
|
|
||||||
|
import { CheckCircleIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
const setting = await getSetting('cache_time');
|
||||||
const cacheTime = setting;
|
const cacheTime = setting;
|
||||||
|
|
||||||
export function CacheTimeSetting() {
|
export function CacheTimeSetting() {
|
||||||
const [time, setTime] = useState(cacheTime);
|
const [time, setTime] = useState(cacheTime);
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
await updateSetting("cache_time", time);
|
await updateSetting('cache_time', time);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-5 py-4 inline-flex items-center justify-between">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="leading-none font-medium text-zinc-200">
|
<span className="font-medium leading-none text-zinc-200">Cache time</span>
|
||||||
Cache time
|
<span className="text-sm leading-none text-zinc-400">
|
||||||
</span>
|
|
||||||
<span className="leading-none text-sm text-zinc-400">
|
|
||||||
The length of time before inactive data gets removed from the cache
|
The length of time before inactive data gets removed from the cache
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,14 +28,14 @@ export function CacheTimeSetting() {
|
|||||||
onChange={(e) => setTime(e.currentTarget.value)}
|
onChange={(e) => setTime(e.currentTarget.value)}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
className="w-24 h-8 rounded-md px-2 bg-zinc-800 text-zinc-300 text-right font-medium focus:outline-none"
|
className="h-8 w-24 rounded-md bg-zinc-800 px-2 text-right font-medium text-zinc-300 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => update()}
|
onClick={() => update()}
|
||||||
className="w-8 h-8 inline-flex items-center justify-center font-medium bg-zinc-800 hover:bg-fuchsia-500 rounded-md"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
<CheckCircleIcon className="w-4 h-4 text-zinc-100" />
|
<CheckCircleIcon className="h-4 w-4 text-zinc-100" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { RefreshIcon } from "@shared/icons";
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
|
||||||
|
import { RefreshIcon } from '@shared/icons';
|
||||||
|
|
||||||
const appVersion = await getVersion();
|
const appVersion = await getVersion();
|
||||||
|
|
||||||
export function VersionSetting() {
|
export function VersionSetting() {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 py-4 inline-flex items-center justify-between">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="leading-none font-medium text-zinc-200">Version</span>
|
<span className="font-medium leading-none text-zinc-200">Version</span>
|
||||||
<span className="leading-none text-sm text-zinc-400">
|
<span className="text-sm leading-none text-zinc-400">
|
||||||
You're using latest version
|
You're using latest version
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
<span className="text-zinc-300 font-medium">{appVersion}</span>
|
<span className="font-medium text-zinc-300">{appVersion}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-8 h-8 inline-flex items-center justify-center font-medium bg-zinc-800 hover:bg-fuchsia-500 rounded-md"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
<RefreshIcon className="w-4 h-4 text-zinc-100" />
|
<RefreshIcon className="h-4 w-4 text-zinc-100" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { AutoStartSetting } from "@app/settings/components/autoStart";
|
import { AutoStartSetting } from '@app/settings/components/autoStart';
|
||||||
import { CacheTimeSetting } from "@app/settings/components/cacheTime";
|
import { CacheTimeSetting } from '@app/settings/components/cacheTime';
|
||||||
import { VersionSetting } from "@app/settings/components/version";
|
import { VersionSetting } from '@app/settings/components/version';
|
||||||
|
|
||||||
export function GeneralSettingsScreen() {
|
export function GeneralSettingsScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full px-3 pt-12">
|
<div className="h-full w-full px-3 pt-12">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-semibold text-zinc-100">General</h1>
|
<h1 className="text-lg font-semibold text-zinc-100">General</h1>
|
||||||
<div className="w-full bg-zinc-900 border-t border-zinc-800/50 rounded-xl">
|
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="w-full h-full flex flex-col divide-y divide-zinc-800">
|
<div className="flex h-full w-full flex-col divide-y divide-zinc-800">
|
||||||
<AutoStartSetting />
|
<AutoStartSetting />
|
||||||
<CacheTimeSetting />
|
<CacheTimeSetting />
|
||||||
<VersionSetting />
|
<VersionSetting />
|
||||||
|
|||||||
@@ -1,104 +1,84 @@
|
|||||||
import { CommandIcon } from "@shared/icons";
|
import { CommandIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function ShortcutsSettingsScreen() {
|
export function ShortcutsSettingsScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full px-3 pt-12">
|
<div className="h-full w-full px-3 pt-12">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-semibold text-zinc-100">Shortcuts</h1>
|
<h1 className="text-lg font-semibold text-zinc-100">Shortcuts</h1>
|
||||||
<div className="w-full bg-zinc-900 border-t border-zinc-800/50 rounded-xl">
|
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="w-full h-full flex flex-col divide-y divide-zinc-800">
|
<div className="flex h-full w-full flex-col divide-y divide-zinc-800">
|
||||||
<div className="px-5 py-4 inline-flex items-center justify-between">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="leading-none font-medium text-zinc-200">
|
<span className="font-medium leading-none text-zinc-200">
|
||||||
Open composer
|
Open composer
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<CommandIcon
|
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
className="text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<span className="text-zinc-500 text-sm leading-none">N</span>
|
<span className="text-sm leading-none text-zinc-500">N</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4 inline-flex items-center justify-between">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="leading-none font-medium text-zinc-200">
|
<span className="font-medium leading-none text-zinc-200">
|
||||||
Add image block
|
Add image block
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<CommandIcon
|
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
className="text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<span className="text-zinc-500 text-sm leading-none">I</span>
|
<span className="text-sm leading-none text-zinc-500">I</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4 inline-flex items-center justify-between">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="leading-none font-medium text-zinc-200">
|
<span className="font-medium leading-none text-zinc-200">
|
||||||
Add newsfeed block
|
Add newsfeed block
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<CommandIcon
|
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
className="text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<span className="text-zinc-500 text-sm leading-none">F</span>
|
<span className="text-sm leading-none text-zinc-500">F</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4 inline-flex items-center justify-between">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="leading-none font-medium text-zinc-200">
|
<span className="font-medium leading-none text-zinc-200">
|
||||||
Open personal page
|
Open personal page
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<CommandIcon
|
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
className="text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<span className="text-zinc-500 text-sm leading-none">P</span>
|
<span className="text-sm leading-none text-zinc-500">P</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4 inline-flex items-center justify-between">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="leading-none font-medium text-zinc-200">
|
<span className="font-medium leading-none text-zinc-200">
|
||||||
Open notification
|
Open notification
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<CommandIcon
|
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
className="text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<span className="text-zinc-500 text-sm leading-none">B</span>
|
<span className="text-sm leading-none text-zinc-500">B</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AddFeedBlock } from "@app/space/components/addFeed";
|
import { AddFeedBlock } from '@app/space/components/addFeed';
|
||||||
import { AddImageBlock } from "@app/space/components/addImage";
|
import { AddImageBlock } from '@app/space/components/addImage';
|
||||||
|
|
||||||
export function AddBlock() {
|
export function AddBlock() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import { User } from "@app/auth/components/user";
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Combobox } from '@headlessui/react';
|
||||||
import { Combobox } from "@headlessui/react";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { createBlock } from "@libs/storage";
|
import { nip19 } from 'nostr-tools';
|
||||||
import { CancelIcon, CheckCircleIcon, CommandIcon } from "@shared/icons";
|
import { Fragment, useState } from 'react';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { useForm } from 'react-hook-form';
|
||||||
import { ADD_FEEDBLOCK_SHORTCUT } from "@stores/shortcuts";
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { User } from '@app/auth/components/user';
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { createBlock } from '@libs/storage';
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function AddFeedBlock() {
|
export function AddFeedBlock() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -18,7 +23,7 @@ export function AddFeedBlock() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
const { status, account } = useAccount();
|
const { status, account } = useAccount();
|
||||||
|
|
||||||
@@ -37,7 +42,7 @@ export function AddFeedBlock() {
|
|||||||
return createBlock(data.kind, data.title, data.content);
|
return createBlock(data.kind, data.title, data.content);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +57,7 @@ export function AddFeedBlock() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
selected.forEach((item, index) => {
|
selected.forEach((item, index) => {
|
||||||
if (item.substring(0, 4) === "npub") {
|
if (item.substring(0, 4) === 'npub') {
|
||||||
selected[index] = nip19.decode(item).data;
|
selected[index] = nip19.decode(item).data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -76,14 +81,14 @@ export function AddFeedBlock() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
className="inline-flex w-56 h-9 items-center justify-start gap-2.5 rounded-md px-2.5"
|
className="inline-flex h-9 w-56 items-center justify-start gap-2.5 rounded-md px-2.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<span className="text-zinc-500 text-sm leading-none">F</span>
|
<span className="text-sm leading-none text-zinc-500">F</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -128,60 +133,53 @@ export function AddFeedBlock() {
|
|||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<CancelIcon
|
<CancelIcon width={14} height={14} className="text-zinc-300" />
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
className="text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
||||||
Specific newsfeed space for people you want to keep up to
|
Specific newsfeed space for people you want to keep up to date
|
||||||
date
|
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex h-full w-full flex-col gap-4 mb-0"
|
className="mb-0 flex h-full w-full flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="title"
|
||||||
|
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Title *
|
Title *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={"text"}
|
type={'text'}
|
||||||
{...register("title", {
|
{...register('title', {
|
||||||
required: true,
|
required: true,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-md px-3 py-2 !outline-none placeholder:text-zinc-500 bg-zinc-800 text-zinc-100"
|
className="relative h-10 w-full rounded-md bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
<span className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
||||||
Choose at least 1 user *
|
Choose at least 1 user *
|
||||||
</label>
|
</span>
|
||||||
<div className="w-full h-[300px] flex flex-col rounded-lg border-t border-zinc-700/50 bg-zinc-800 overflow-x-hidden overflow-y-auto">
|
<div className="flex h-[300px] w-full flex-col overflow-y-auto overflow-x-hidden rounded-lg border-t border-zinc-700/50 bg-zinc-800">
|
||||||
<div className="w-full px-3 py-2">
|
<div className="w-full px-3 py-2">
|
||||||
<Combobox
|
<Combobox value={selected} onChange={setSelected} multiple>
|
||||||
value={selected}
|
|
||||||
onChange={setSelected}
|
|
||||||
multiple
|
|
||||||
>
|
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoFocus={false}
|
|
||||||
placeholder="Enter pubkey or npub..."
|
placeholder="Enter pubkey or npub..."
|
||||||
className="mb-2 relative h-10 w-full rounded-md px-3 py-2 !outline-none placeholder:text-zinc-500 bg-zinc-700 text-zinc-100"
|
className="relative mb-2 h-10 w-full rounded-md bg-zinc-700 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<Combobox.Options static>
|
<Combobox.Options static>
|
||||||
{query.length > 0 && (
|
{query.length > 0 && (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
value={query}
|
value={query}
|
||||||
className="group w-full flex items-center justify-between px-2 py-2 rounded-md hover:bg-zinc-700"
|
className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
@@ -189,7 +187,7 @@ export function AddFeedBlock() {
|
|||||||
<img
|
<img
|
||||||
alt={query}
|
alt={query}
|
||||||
src={DEFAULT_AVATAR}
|
src={DEFAULT_AVATAR}
|
||||||
className="w-11 h-11 shrink-0 object-cover rounded"
|
className="h-11 w-11 shrink-0 rounded object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<span className="text-base leading-tight text-zinc-400">
|
<span className="text-base leading-tight text-zinc-400">
|
||||||
@@ -198,26 +196,26 @@ export function AddFeedBlock() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selected && (
|
{selected && (
|
||||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
)}
|
)}
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
JSON.parse(account.follows).map((follow) => (
|
JSON.parse(account.follows).map((follow) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={follow}
|
key={follow}
|
||||||
value={follow}
|
value={follow}
|
||||||
className="group w-full flex items-center justify-between px-2 py-2 rounded-md hover:bg-zinc-700"
|
className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
<User pubkey={follow} />
|
<User pubkey={follow} />
|
||||||
{selected && (
|
{selected && (
|
||||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -233,32 +231,12 @@ export function AddFeedBlock() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<svg
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<title id="loading">Loading</title>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
) : (
|
||||||
"Confirm"
|
'Confirm'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { createBlock } from "@libs/storage";
|
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { CancelIcon, CommandIcon } from "@shared/icons";
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
import { Image } from "@shared/image";
|
import { Body, fetch } from '@tauri-apps/api/http';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { Fragment, useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { useForm } from 'react-hook-form';
|
||||||
import { ADD_IMAGEBLOCK_SHORTCUT } from "@stores/shortcuts";
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { open } from "@tauri-apps/api/dialog";
|
import { createBlock } from '@libs/storage';
|
||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
|
||||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
import { CancelIcon, CommandIcon } from '@shared/icons';
|
||||||
import { dateToUnix } from "@utils/date";
|
import { Image } from '@shared/image';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||||
|
|
||||||
|
import { createBlobFromFile } from '@utils/createBlobFromFile';
|
||||||
|
import { dateToUnix } from '@utils/date';
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function AddImageBlock() {
|
export function AddImageBlock() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -22,7 +26,7 @@ export function AddImageBlock() {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [image, setImage] = useState("");
|
const [image, setImage] = useState('');
|
||||||
|
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
|
|
||||||
@@ -51,8 +55,8 @@ export function AddImageBlock() {
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: "Image",
|
name: 'Image',
|
||||||
extensions: ["png", "jpeg", "jpg"],
|
extensions: ['png', 'jpeg', 'jpg'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -62,19 +66,19 @@ export function AddImageBlock() {
|
|||||||
} else if (selected === null) {
|
} else if (selected === null) {
|
||||||
// user cancelled the selection
|
// user cancelled the selection
|
||||||
} else {
|
} else {
|
||||||
const filename = selected.split("/").pop();
|
const filename = selected.split('/').pop();
|
||||||
const file = await createBlobFromFile(selected);
|
const file = await createBlobFromFile(selected);
|
||||||
const buf = await file.arrayBuffer();
|
const buf = await file.arrayBuffer();
|
||||||
|
|
||||||
const res: any = await fetch("https://void.cat/upload?cli=false", {
|
const res: any = await fetch('https://void.cat/upload?cli=false', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
headers: {
|
headers: {
|
||||||
accept: "*/*",
|
accept: '*/*',
|
||||||
"Content-Type": "application/octet-stream",
|
'Content-Type': 'application/octet-stream',
|
||||||
"V-Filename": filename,
|
'V-Filename': filename,
|
||||||
"V-Description": "Upload from https://lume.nu",
|
'V-Description': 'Upload from https://lume.nu',
|
||||||
"V-Strip-Metadata": "true",
|
'V-Strip-Metadata': 'true',
|
||||||
},
|
},
|
||||||
body: Body.bytes(buf),
|
body: Body.bytes(buf),
|
||||||
});
|
});
|
||||||
@@ -82,11 +86,11 @@ export function AddImageBlock() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const imageURL = `https://void.cat/d/${res.data.file.id}.webp`;
|
const imageURL = `https://void.cat/d/${res.data.file.id}.webp`;
|
||||||
tags.current = [
|
tags.current = [
|
||||||
["url", imageURL],
|
['url', imageURL],
|
||||||
["m", res.data.file.metadata.mimeType],
|
['m', res.data.file.metadata.mimeType],
|
||||||
["x", res.data.file.metadata.digest],
|
['x', res.data.file.metadata.digest],
|
||||||
["size", res.data.file.metadata.size],
|
['size', res.data.file.metadata.size],
|
||||||
["magnet", res.data.file.metadata.magnetLink],
|
['magnet', res.data.file.metadata.magnetLink],
|
||||||
];
|
];
|
||||||
|
|
||||||
setImage(imageURL);
|
setImage(imageURL);
|
||||||
@@ -99,7 +103,7 @@ export function AddImageBlock() {
|
|||||||
return createBlock(data.kind, data.title, data.content);
|
return createBlock(data.kind, data.title, data.content);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,7 +135,7 @@ export function AddImageBlock() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue("content", image);
|
setValue('content', image);
|
||||||
}, [setValue, image]);
|
}, [setValue, image]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -139,14 +143,14 @@ export function AddImageBlock() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
className="inline-flex w-56 h-9 items-center justify-start gap-2.5 rounded-md px-2.5"
|
className="inline-flex h-9 w-56 items-center justify-start gap-2.5 rounded-md px-2.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<span className="text-zinc-500 text-sm leading-none">I</span>
|
<span className="text-sm leading-none text-zinc-500">I</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -191,48 +195,49 @@ export function AddImageBlock() {
|
|||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<CancelIcon
|
<CancelIcon width={14} height={14} className="text-zinc-300" />
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
className="text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
||||||
Pin your favorite image to Space then you can view every
|
Pin your favorite image to Space then you can view every time that
|
||||||
time that you use Lume, your image will be broadcast to
|
you use Lume, your image will be broadcast to Nostr Relay as well
|
||||||
Nostr Relay as well
|
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex h-full w-full flex-col gap-4 mb-0"
|
className="mb-0 flex h-full w-full flex-col gap-4"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type={"hidden"}
|
type={'hidden'}
|
||||||
{...register("content")}
|
{...register('content')}
|
||||||
value={image}
|
value={image}
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="title"
|
||||||
|
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Title *
|
Title *
|
||||||
</label>
|
</label>
|
||||||
<div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
<div className="after:shadow-highlight relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||||
<input
|
<input
|
||||||
type={"text"}
|
type={'text'}
|
||||||
{...register("title", {
|
{...register('title', {
|
||||||
required: true,
|
required: true,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
className="shadow-input relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="picture"
|
||||||
|
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Picture
|
Picture
|
||||||
</label>
|
</label>
|
||||||
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
||||||
@@ -240,7 +245,7 @@ export function AddImageBlock() {
|
|||||||
src={image}
|
src={image}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt="content"
|
alt="content"
|
||||||
className="relative z-10 max-h-[156px] h-auto w-[150px] object-cover rounded-md"
|
className="relative z-10 h-auto max-h-[156px] w-[150px] rounded-md object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-3 right-3 z-10">
|
<div className="absolute bottom-3 right-3 z-10">
|
||||||
<button
|
<button
|
||||||
@@ -257,7 +262,7 @@ export function AddImageBlock() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<svg
|
<svg
|
||||||
@@ -282,7 +287,7 @@ export function AddImageBlock() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
) : (
|
) : (
|
||||||
"Confirm"
|
'Confirm'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { getNotesByAuthors, removeBlock } from "@libs/storage";
|
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Note } from "@shared/notes/note";
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { useEffect, useRef } from 'react';
|
||||||
import { TitleBar } from "@shared/titleBar";
|
|
||||||
import {
|
import { getNotesByAuthors, removeBlock } from '@libs/storage';
|
||||||
useInfiniteQuery,
|
|
||||||
useMutation,
|
import { Note } from '@shared/notes/note';
|
||||||
useQueryClient,
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
} from "@tanstack/react-query";
|
import { TitleBar } from '@shared/titleBar';
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
const ITEM_PER_PAGE = 10;
|
const ITEM_PER_PAGE = 10;
|
||||||
|
|
||||||
@@ -16,13 +14,9 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage }: any =
|
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage }: any =
|
||||||
useInfiniteQuery({
|
useInfiniteQuery({
|
||||||
queryKey: ["newsfeed", params.content],
|
queryKey: ['newsfeed', params.content],
|
||||||
queryFn: async ({ pageParam = 0 }) => {
|
queryFn: async ({ pageParam = 0 }) => {
|
||||||
return await getNotesByAuthors(
|
return await getNotesByAuthors(params.content, ITEM_PER_PAGE, pageParam);
|
||||||
params.content,
|
|
||||||
ITEM_PER_PAGE,
|
|
||||||
pageParam,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
});
|
});
|
||||||
@@ -46,11 +40,7 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (lastItem.index >= notes.length - 1 && hasNextPage && !isFetchingNextPage) {
|
||||||
lastItem.index >= notes.length - 1 &&
|
|
||||||
hasNextPage &&
|
|
||||||
!isFetchingNextPage
|
|
||||||
) {
|
|
||||||
fetchNextPage();
|
fetchNextPage();
|
||||||
}
|
}
|
||||||
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||||
@@ -60,7 +50,7 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
return removeBlock(id);
|
return removeBlock(id);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,14 +66,14 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
<div className="w-[400px] shrink-0 border-r border-zinc-900">
|
||||||
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
|
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5"
|
||||||
style={{ contain: "strict" }}
|
style={{ contain: 'strict' }}
|
||||||
>
|
>
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
@@ -100,8 +90,7 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
className="absolute left-0 top-0 w-full"
|
className="absolute left-0 top-0 w-full"
|
||||||
style={{
|
style={{
|
||||||
transform: `translateY(${
|
transform: `translateY(${
|
||||||
itemsVirtualizer[0].start -
|
itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
|
||||||
rowVirtualizer.options.scrollMargin
|
|
||||||
}px)`,
|
}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { useNewsfeed } from "@app/space/hooks/useNewsfeed";
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { getNotes } from "@libs/storage";
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { Note } from "@shared/notes/note";
|
import { useEffect, useRef } from 'react';
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { useNewsfeed } from '@app/space/hooks/useNewsfeed';
|
||||||
import { useNote } from "@stores/note";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { getNotes } from '@libs/storage';
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { Note } from '@shared/notes/note';
|
||||||
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
|
import { TitleBar } from '@shared/titleBar';
|
||||||
|
|
||||||
|
import { useNote } from '@stores/note';
|
||||||
|
|
||||||
const ITEM_PER_PAGE = 10;
|
const ITEM_PER_PAGE = 10;
|
||||||
|
|
||||||
@@ -18,15 +22,9 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
state.toggleHasNewNote,
|
state.toggleHasNewNote,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const {
|
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch }: any =
|
||||||
status,
|
useInfiniteQuery({
|
||||||
data,
|
queryKey: ['newsfeed-circle'],
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
refetch,
|
|
||||||
}: any = useInfiniteQuery({
|
|
||||||
queryKey: ["newsfeed-circle"],
|
|
||||||
queryFn: async ({ pageParam = 0 }) => {
|
queryFn: async ({ pageParam = 0 }) => {
|
||||||
return await getNotes(ITEM_PER_PAGE, pageParam);
|
return await getNotes(ITEM_PER_PAGE, pageParam);
|
||||||
},
|
},
|
||||||
@@ -52,11 +50,7 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (lastItem.index >= notes.length - 1 && hasNextPage && !isFetchingNextPage) {
|
||||||
lastItem.index >= notes.length - 1 &&
|
|
||||||
hasNextPage &&
|
|
||||||
!isFetchingNextPage
|
|
||||||
) {
|
|
||||||
fetchNextPage();
|
fetchNextPage();
|
||||||
}
|
}
|
||||||
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||||
@@ -81,14 +75,14 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 relative w-[400px] border-r border-zinc-900">
|
<div className="relative w-[400px] shrink-0 border-r border-zinc-900">
|
||||||
<TitleBar title="Your Circle" />
|
<TitleBar title="Your Circle" />
|
||||||
{hasNewNote && (
|
{hasNewNote && (
|
||||||
<div className="z-50 absolute top-12 left-1/2 transform -translate-x-1/2">
|
<div className="absolute left-1/2 top-12 z-50 -translate-x-1/2 transform">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => refreshFirstPage()}
|
onClick={() => refreshFirstPage()}
|
||||||
className="inline-flex items-center justify-center w-min px-3.5 py-1.5 rounded-full bg-fuchsia-500 hover:bg-fuchsia-600 border border-fuchsia-800/50 text-sm"
|
className="inline-flex w-min items-center justify-center rounded-full border border-fuchsia-800/50 bg-fuchsia-500 px-3.5 py-1.5 text-sm hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
Newest
|
Newest
|
||||||
</button>
|
</button>
|
||||||
@@ -96,10 +90,10 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5"
|
||||||
style={{ contain: "strict" }}
|
style={{ contain: 'strict' }}
|
||||||
>
|
>
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
@@ -116,8 +110,7 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
className="absolute left-0 top-0 w-full"
|
className="absolute left-0 top-0 w-full"
|
||||||
style={{
|
style={{
|
||||||
transform: `translateY(${
|
transform: `translateY(${
|
||||||
itemsVirtualizer[0].start -
|
itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
|
||||||
rowVirtualizer.options.scrollMargin
|
|
||||||
}px)`,
|
}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { removeBlock } from "@libs/storage";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { CancelIcon } from "@shared/icons";
|
|
||||||
import { Image } from "@shared/image";
|
import { removeBlock } from '@libs/storage';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { CancelIcon } from '@shared/icons';
|
||||||
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
export function ImageBlock({ params }: { params: any }) {
|
export function ImageBlock({ params }: { params: any }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -12,22 +15,20 @@ export function ImageBlock({ params }: { params: any }) {
|
|||||||
return removeBlock(id);
|
return removeBlock(id);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[350px] h-full flex flex-col justify-between border-r border-zinc-900">
|
<div className="flex h-full w-[350px] shrink-0 flex-col justify-between border-r border-zinc-900">
|
||||||
<div className="relative flex-1 w-full h-full p-3 overflow-hidden">
|
<div className="relative h-full w-full flex-1 overflow-hidden p-3">
|
||||||
<div className="absolute top-3 left-0 w-full h-16 px-3">
|
<div className="absolute left-0 top-3 h-16 w-full px-3">
|
||||||
<div className="h-16 rounded-t-xl overflow-hidden flex items-center justify-between px-5">
|
<div className="flex h-16 items-center justify-between overflow-hidden rounded-t-xl px-5">
|
||||||
<h3 className="text-white font-medium drop-shadow-lg">
|
<h3 className="font-medium text-white drop-shadow-lg">{params.title}</h3>
|
||||||
{params.title}
|
|
||||||
</h3>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => block.mutate(params.id)}
|
onClick={() => block.mutate(params.id)}
|
||||||
className="inline-flex h-7 w-7 rounded-md items-center justify-center bg-white/30 backdrop-blur-lg"
|
className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-white/30 backdrop-blur-lg"
|
||||||
>
|
>
|
||||||
<CancelIcon width={16} height={16} className="text-white" />
|
<CancelIcon width={16} height={16} className="text-white" />
|
||||||
</button>
|
</button>
|
||||||
@@ -37,7 +38,7 @@ export function ImageBlock({ params }: { params: any }) {
|
|||||||
src={params.content}
|
src={params.content}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt={params.title}
|
alt={params.title}
|
||||||
className="w-full h-full object-cover rounded-xl border-t border-zinc-800/50"
|
className="h-full w-full rounded-xl border-t border-zinc-800/50 object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { useLiveThread } from "@app/space/hooks/useLiveThread";
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { getNoteByID, removeBlock } from "@libs/storage";
|
|
||||||
import { Kind1 } from "@shared/notes/contents/kind1";
|
import { useLiveThread } from '@app/space/hooks/useLiveThread';
|
||||||
import { Kind1063 } from "@shared/notes/contents/kind1063";
|
|
||||||
import { NoteMetadata } from "@shared/notes/metadata";
|
import { getNoteByID, removeBlock } from '@libs/storage';
|
||||||
import { NoteReplyForm } from "@shared/notes/replies/form";
|
|
||||||
import { RepliesList } from "@shared/notes/replies/list";
|
import { Kind1 } from '@shared/notes/contents/kind1';
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { Kind1063 } from '@shared/notes/contents/kind1063';
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { NoteMetadata } from '@shared/notes/metadata';
|
||||||
import { User } from "@shared/user";
|
import { NoteReplyForm } from '@shared/notes/replies/form';
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { RepliesList } from '@shared/notes/replies/list';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
import { parser } from "@utils/parser";
|
import { TitleBar } from '@shared/titleBar';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
import { parser } from '@utils/parser';
|
||||||
|
|
||||||
export function ThreadBlock({ params }: { params: any }) {
|
export function ThreadBlock({ params }: { params: any }) {
|
||||||
useLiveThread(params.content);
|
useLiveThread(params.content);
|
||||||
@@ -18,9 +22,9 @@ export function ThreadBlock({ params }: { params: any }) {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
const { status, data } = useQuery(["thread", params.content], async () => {
|
const { status, data } = useQuery(['thread', params.content], async () => {
|
||||||
const res = await getNoteByID(params.content);
|
const res = await getNoteByID(params.content);
|
||||||
res["content"] = parser(res);
|
res['content'] = parser(res);
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,17 +33,17 @@ export function ThreadBlock({ params }: { params: any }) {
|
|||||||
return removeBlock(id);
|
return removeBlock(id);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
<div className="w-[400px] shrink-0 border-r border-zinc-900">
|
||||||
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
|
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
|
||||||
<div className="scrollbar-hide flex w-full h-full flex-col gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
<div className="scrollbar-hide flex h-full w-full flex-col gap-1.5 overflow-y-auto pb-20 pt-1.5">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
<div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,12 +60,9 @@ export function ThreadBlock({ params }: { params: any }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 bg-zinc-900 rounded-md">
|
<div className="mt-3 rounded-md bg-zinc-900">
|
||||||
{account && (
|
{account && (
|
||||||
<NoteReplyForm
|
<NoteReplyForm rootID={params.content} userPubkey={account.pubkey} />
|
||||||
rootID={params.content}
|
|
||||||
userPubkey={account.pubkey}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { createReplyNote } from "@libs/storage";
|
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||||
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { useContext, useEffect, useRef } from 'react';
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useContext, useEffect, useRef } from "react";
|
import { createReplyNote } from '@libs/storage';
|
||||||
|
|
||||||
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
|
|
||||||
export function useLiveThread(id: string) {
|
export function useLiveThread(id: string) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -18,24 +20,24 @@ export function useLiveThread(id: string) {
|
|||||||
data.kind,
|
data.kind,
|
||||||
data.tags,
|
data.tags,
|
||||||
data.content,
|
data.content,
|
||||||
data.created_at,
|
data.created_at
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["replies", id] });
|
queryClient.invalidateQueries({ queryKey: ['replies', id] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
"#e": [id],
|
'#e': [id],
|
||||||
since: now.current,
|
since: now.current,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sub = ndk.subscribe(filter, { closeOnEose: false });
|
const sub = ndk.subscribe(filter, { closeOnEose: false });
|
||||||
|
|
||||||
sub.addListener("event", (event: NDKEvent) => {
|
sub.addListener('event', (event: NDKEvent) => {
|
||||||
thread.mutate(event);
|
thread.mutate(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { createNote } from "@libs/storage";
|
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||||
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
import { useContext, useEffect, useRef } from 'react';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { useNote } from "@stores/note";
|
import { createNote } from '@libs/storage';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
import { useContext, useEffect, useRef } from "react";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
|
|
||||||
|
import { useNote } from '@stores/note';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function useNewsfeed() {
|
export function useNewsfeed() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
@@ -14,7 +18,7 @@ export function useNewsfeed() {
|
|||||||
const { status, account } = useAccount();
|
const { status, account } = useAccount();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "success" && account) {
|
if (status === 'success' && account) {
|
||||||
const follows = account ? JSON.parse(account.follows) : [];
|
const follows = account ? JSON.parse(account.follows) : [];
|
||||||
|
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
@@ -25,8 +29,8 @@ export function useNewsfeed() {
|
|||||||
|
|
||||||
sub.current = ndk.subscribe(filter, { closeOnEose: false });
|
sub.current = ndk.subscribe(filter, { closeOnEose: false });
|
||||||
|
|
||||||
sub.current.addListener("event", (event: NDKEvent) => {
|
sub.current.addListener('event', (event: NDKEvent) => {
|
||||||
console.log("new note: ", event);
|
console.log('new note: ', event);
|
||||||
// add to db
|
// add to db
|
||||||
createNote(
|
createNote(
|
||||||
event.id,
|
event.id,
|
||||||
@@ -34,7 +38,7 @@ export function useNewsfeed() {
|
|||||||
event.kind,
|
event.kind,
|
||||||
event.tags,
|
event.tags,
|
||||||
event.content,
|
event.content,
|
||||||
event.created_at,
|
event.created_at
|
||||||
);
|
);
|
||||||
// notify user about created note
|
// notify user about created note
|
||||||
toggleHasNewNote(true);
|
toggleHasNewNote(true);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { AddBlock } from "@app/space/components/add";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { FeedBlock } from "@app/space/components/blocks/feed";
|
|
||||||
import { FollowingBlock } from "@app/space/components/blocks/following";
|
import { AddBlock } from '@app/space/components/add';
|
||||||
import { ImageBlock } from "@app/space/components/blocks/image";
|
import { FeedBlock } from '@app/space/components/blocks/feed';
|
||||||
import { ThreadBlock } from "@app/space/components/blocks/thread";
|
import { FollowingBlock } from '@app/space/components/blocks/following';
|
||||||
import { getBlocks } from "@libs/storage";
|
import { ImageBlock } from '@app/space/components/blocks/image';
|
||||||
import { LoaderIcon } from "@shared/icons";
|
import { ThreadBlock } from '@app/space/components/blocks/thread';
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
|
import { getBlocks } from '@libs/storage';
|
||||||
|
|
||||||
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function SpaceScreen() {
|
export function SpaceScreen() {
|
||||||
const {
|
const {
|
||||||
@@ -13,7 +16,7 @@ export function SpaceScreen() {
|
|||||||
data: blocks,
|
data: blocks,
|
||||||
isFetching,
|
isFetching,
|
||||||
} = useQuery(
|
} = useQuery(
|
||||||
["blocks"],
|
['blocks'],
|
||||||
async () => {
|
async () => {
|
||||||
return await getBlocks();
|
return await getBlocks();
|
||||||
},
|
},
|
||||||
@@ -22,20 +25,20 @@ export function SpaceScreen() {
|
|||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
<div className="scrollbar-hide flex h-full w-full flex-nowrap overflow-x-auto overflow-y-hidden">
|
||||||
<FollowingBlock block={1} />
|
<FollowingBlock block={1} />
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
|
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="group overflow-hidden h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
|
className="group flex h-11 w-full items-center justify-between overflow-hidden border-b border-zinc-900 px-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full flex-1 flex items-center justify-center p-3">
|
<div className="flex w-full flex-1 items-center justify-center p-3">
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,23 +57,23 @@ export function SpaceScreen() {
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
{isFetching && (
|
{isFetching && (
|
||||||
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
|
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="group overflow-hidden h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
|
className="group flex h-11 w-full items-center justify-between overflow-hidden border-b border-zinc-900 px-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full flex-1 flex items-center justify-center p-3">
|
<div className="flex w-full flex-1 items-center justify-center p-3">
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
|
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
|
||||||
<div className="w-full h-full inline-flex items-center justify-center">
|
<div className="inline-flex h-full w-full items-center justify-center">
|
||||||
<AddBlock />
|
<AddBlock />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 w-[350px]" />
|
<div className="w-[350px] shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { FollowIcon, LoaderIcon, UnfollowIcon } from "@shared/icons";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Image } from "@shared/image";
|
import { useEffect, useState } from 'react';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { FollowIcon, LoaderIcon, UnfollowIcon } from '@shared/icons';
|
||||||
import { useSocial } from "@utils/hooks/useSocial";
|
import { Image } from '@shared/image';
|
||||||
import { compactNumber } from "@utils/number";
|
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
import { useSocial } from '@utils/hooks/useSocial';
|
||||||
|
import { compactNumber } from '@utils/number';
|
||||||
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function Profile({ data }: { data: any }) {
|
export function Profile({ data }: { data: any }) {
|
||||||
const { status, data: userStats } = useQuery(
|
const { status, data: userStats } = useQuery(['user-stats', data.pubkey], async () => {
|
||||||
["user-stats", data.pubkey],
|
const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
|
||||||
async () => {
|
|
||||||
const res = await fetch(
|
|
||||||
`https://api.nostr.band/v0/stats/profile/${data.pubkey}`,
|
|
||||||
);
|
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
|
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
|
||||||
const profile = embedProfile;
|
const profile = embedProfile;
|
||||||
@@ -45,7 +43,7 @@ export function Profile({ data }: { data: any }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "success" && userFollows) {
|
if (status === 'success' && userFollows) {
|
||||||
if (userFollows.includes(data.pubkey)) {
|
if (userFollows.includes(data.pubkey)) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
}
|
}
|
||||||
@@ -55,7 +53,7 @@ export function Profile({ data }: { data: any }) {
|
|||||||
if (!profile)
|
if (!profile)
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md bg-zinc-900 px-5 py-5">
|
<div className="rounded-md bg-zinc-900 px-5 py-5">
|
||||||
<p>Can't fetch profile</p>
|
<p>Can't fetch profile</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -63,27 +61,27 @@ export function Profile({ data }: { data: any }) {
|
|||||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-5 py-5">
|
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-5 py-5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
<div className="w-11 h-11 shrink-0">
|
<div className="h-11 w-11 shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={profile.picture}
|
src={profile.picture}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
className="w-11 h-11 object-cover rounded-lg"
|
className="h-11 w-11 rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<h3 className="max-w-[15rem] truncate font-semibold text-zinc-100 leading-none">
|
<h3 className="max-w-[15rem] truncate font-semibold leading-none text-zinc-100">
|
||||||
{profile.display_name || profile.name}
|
{profile.display_name || profile.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="max-w-[10rem] truncate text-sm text-zinc-400 leading-none">
|
<p className="max-w-[10rem] truncate text-sm leading-none text-zinc-400">
|
||||||
{profile.nip05 || shortenKey(data.pubkey)}
|
{profile.nip05 || shortenKey(data.pubkey)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
{socialStatus === "loading" ? (
|
{socialStatus === 'loading' ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex w-8 h-8 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
</button>
|
</button>
|
||||||
@@ -91,17 +89,17 @@ export function Profile({ data }: { data: any }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => unfollowUser(data.pubkey)}
|
onClick={() => unfollowUser(data.pubkey)}
|
||||||
className="inline-flex w-8 h-8 items-center justify-center rounded-md text-zinc-400 bg-zinc-800 hover:bg-fuchsia-500 hover:text-white"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-fuchsia-500 hover:text-white"
|
||||||
>
|
>
|
||||||
<UnfollowIcon className="w-4 h-4" />
|
<UnfollowIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => followUser(data.pubkey)}
|
onClick={() => followUser(data.pubkey)}
|
||||||
className="inline-flex w-8 h-8 items-center justify-center rounded-md text-zinc-400 bg-zinc-800 hover:bg-fuchsia-500 hover:text-white"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-fuchsia-500 hover:text-white"
|
||||||
>
|
>
|
||||||
<FollowIcon className="w-4 h-4" />
|
<FollowIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -112,37 +110,31 @@ export function Profile({ data }: { data: any }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full flex items-center gap-8">
|
<div className="flex w-full items-center gap-8">
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<span className="leading-none font-semibold text-zinc-100">
|
<span className="font-semibold leading-none text-zinc-100">
|
||||||
{userStats.stats[data.pubkey].followers_pubkey_count ?? 0}
|
{userStats.stats[data.pubkey].followers_pubkey_count ?? 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="leading-none text-sm text-zinc-400">
|
<span className="text-sm leading-none text-zinc-400">Followers</span>
|
||||||
Followers
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<span className="leading-none font-semibold text-zinc-100">
|
<span className="font-semibold leading-none text-zinc-100">
|
||||||
{userStats.stats[data.pubkey].pub_following_pubkey_count ?? 0}
|
{userStats.stats[data.pubkey].pub_following_pubkey_count ?? 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="leading-none text-sm text-zinc-400">
|
<span className="text-sm leading-none text-zinc-400">Following</span>
|
||||||
Following
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<span className="leading-none font-semibold text-zinc-100">
|
<span className="font-semibold leading-none text-zinc-100">
|
||||||
{userStats.stats[data.pubkey].zaps_received
|
{userStats.stats[data.pubkey].zaps_received
|
||||||
? compactNumber.format(
|
? compactNumber.format(
|
||||||
userStats.stats[data.pubkey].zaps_received.msats / 1000,
|
userStats.stats[data.pubkey].zaps_received.msats / 1000
|
||||||
)
|
)
|
||||||
: 0}
|
: 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="leading-none text-sm text-zinc-400">
|
<span className="text-sm leading-none text-zinc-400">Zaps received</span>
|
||||||
Zaps received
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
import { Note } from "@shared/notes/note";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { Note } from '@shared/notes/note';
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
|
import { TitleBar } from '@shared/titleBar';
|
||||||
|
|
||||||
export function TrendingNotes() {
|
export function TrendingNotes() {
|
||||||
const { status, data, error } = useQuery(["trending-notes"], async () => {
|
const { status, data, error } = useQuery(['trending-notes'], async () => {
|
||||||
const res = await fetch("https://api.nostr.band/v0/trending/notes");
|
const res = await fetch('https://api.nostr.band/v0/trending/notes');
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Error");
|
throw new Error('Error');
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900">
|
<div className="flex w-[360px] shrink-0 flex-col border-r border-zinc-900">
|
||||||
<TitleBar title="Trending Posts" />
|
<TitleBar title="Trending Posts" />
|
||||||
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
<div className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5">
|
||||||
{error && <p>Failed to fetch</p>}
|
{error && <p>Failed to fetch</p>}
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
<div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative w-full flex flex-col pt-1.5">
|
<div className="relative flex w-full flex-col pt-1.5">
|
||||||
{data.notes.map((item) => (
|
{data.notes.map((item) => (
|
||||||
<Note key={item.id} event={item.event} />
|
<Note key={item.id} event={item.event} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
import { Profile } from "@app/trending/components/profile";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { Profile } from '@app/trending/components/profile';
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
|
import { TitleBar } from '@shared/titleBar';
|
||||||
|
|
||||||
export function TrendingProfiles() {
|
export function TrendingProfiles() {
|
||||||
const { status, data, error } = useQuery(["trending-profiles"], async () => {
|
const { status, data, error } = useQuery(['trending-profiles'], async () => {
|
||||||
const res = await fetch("https://api.nostr.band/v0/trending/profiles");
|
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Error");
|
throw new Error('Error');
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900">
|
<div className="flex w-[360px] shrink-0 flex-col border-r border-zinc-900">
|
||||||
<TitleBar title="Trending Profiles" />
|
<TitleBar title="Trending Profiles" />
|
||||||
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
<div className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5">
|
||||||
{error && <p>Failed to fetch</p>}
|
{error && <p>Failed to fetch</p>}
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
<div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative w-full flex flex-col gap-3 px-3 pt-3">
|
<div className="relative flex w-full flex-col gap-3 px-3 pt-3">
|
||||||
{data.profiles.map((item) => (
|
{data.profiles.map((item) => (
|
||||||
<Profile key={item.pubkey} data={item} />
|
<Profile key={item.pubkey} data={item} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { TrendingNotes } from "@app/trending/components/trendingNotes";
|
import { TrendingNotes } from '@app/trending/components/trendingNotes';
|
||||||
import { TrendingProfiles } from "@app/trending/components/trendingProfiles";
|
import { TrendingProfiles } from '@app/trending/components/trendingProfiles';
|
||||||
|
|
||||||
export function TrendingScreen() {
|
export function TrendingScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
<div className="scrollbar-hide flex h-full w-full flex-nowrap overflow-x-auto overflow-y-hidden">
|
||||||
<TrendingProfiles />
|
<TrendingProfiles />
|
||||||
<TrendingNotes />
|
<TrendingNotes />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { NDKFilter } from "@nostr-dev-kit/ndk";
|
import { NDKFilter } from '@nostr-dev-kit/ndk';
|
||||||
import { Note } from "@shared/notes/note";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { useContext } from 'react';
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { dateToUnix, getHourAgo } from "@utils/date";
|
import { Note } from '@shared/notes/note';
|
||||||
import { LumeEvent } from "@utils/types";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { useContext } from "react";
|
|
||||||
|
import { dateToUnix, getHourAgo } from '@utils/date';
|
||||||
|
import { LumeEvent } from '@utils/types';
|
||||||
|
|
||||||
export function UserFeed({ pubkey }: { pubkey: string }) {
|
export function UserFeed({ pubkey }: { pubkey: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const { status, data } = useQuery(["user-feed", pubkey], async () => {
|
const { status, data } = useQuery(['user-feed', pubkey], async () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
@@ -21,7 +23,7 @@ export function UserFeed({ pubkey }: { pubkey: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-[400px] px-2 pb-10">
|
<div className="w-full max-w-[400px] px-2 pb-10">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,54 +1,49 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { compactNumber } from "@utils/number";
|
|
||||||
|
import { compactNumber } from '@utils/number';
|
||||||
|
|
||||||
export function UserMetadata({ pubkey }: { pubkey: string }) {
|
export function UserMetadata({ pubkey }: { pubkey: string }) {
|
||||||
const { status, data } = useQuery(["user-metadata", pubkey], async () => {
|
const { status, data } = useQuery(['user-metadata', pubkey], async () => {
|
||||||
const res = await fetch(
|
const res = await fetch(`https://api.nostr.band/v0/stats/profile/${pubkey}`);
|
||||||
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
|
|
||||||
);
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Error");
|
throw new Error('Error');
|
||||||
}
|
}
|
||||||
return await res.json();
|
return await res.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === "loading") {
|
if (status === 'loading') {
|
||||||
return <p>Loading...</p>;
|
return <p>Loading...</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex items-center gap-10">
|
<div className="flex w-full items-center gap-10">
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<span className="leading-none font-semibold text-zinc-100">
|
<span className="font-semibold leading-none text-zinc-100">
|
||||||
{data.stats[pubkey].followers_pubkey_count ?? 0}
|
{data.stats[pubkey].followers_pubkey_count ?? 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="leading-none text-sm text-zinc-400">Followers</span>
|
<span className="text-sm leading-none text-zinc-400">Followers</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<span className="leading-none font-semibold text-zinc-100">
|
<span className="font-semibold leading-none text-zinc-100">
|
||||||
{data.stats[pubkey].pub_following_pubkey_count ?? 0}
|
{data.stats[pubkey].pub_following_pubkey_count ?? 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="leading-none text-sm text-zinc-400">Following</span>
|
<span className="text-sm leading-none text-zinc-400">Following</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<span className="leading-none font-semibold text-zinc-100">
|
<span className="font-semibold leading-none text-zinc-100">
|
||||||
{data.stats[pubkey].zaps_received
|
{data.stats[pubkey].zaps_received
|
||||||
? compactNumber.format(
|
? compactNumber.format(data.stats[pubkey].zaps_received.msats / 1000)
|
||||||
data.stats[pubkey].zaps_received.msats / 1000,
|
|
||||||
)
|
|
||||||
: 0}
|
: 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="leading-none text-sm text-zinc-400">
|
<span className="text-sm leading-none text-zinc-400">Zaps received</span>
|
||||||
Zaps received
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex flex-col gap-1">
|
<div className="inline-flex flex-col gap-1">
|
||||||
<span className="leading-none font-semibold text-zinc-100">
|
<span className="font-semibold leading-none text-zinc-100">
|
||||||
{data.stats[pubkey].zaps_sent
|
{data.stats[pubkey].zaps_sent
|
||||||
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
|
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
|
||||||
: 0}
|
: 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="leading-none text-sm text-zinc-400">Zaps sent</span>
|
<span className="text-sm leading-none text-zinc-400">Zaps sent</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { UserFeed } from "@app/user/components/feed";
|
import { Tab } from '@headlessui/react';
|
||||||
import { UserMetadata } from "@app/user/components/metadata";
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
import { Tab } from "@headlessui/react";
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { EditProfileModal } from "@shared/editProfileModal";
|
|
||||||
import { ThreadsIcon, ZapIcon } from "@shared/icons";
|
import { UserFeed } from '@app/user/components/feed';
|
||||||
import { Image } from "@shared/image";
|
import { UserMetadata } from '@app/user/components/metadata';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { EditProfileModal } from '@shared/editProfileModal';
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { ThreadsIcon, ZapIcon } from '@shared/icons';
|
||||||
import { useSocial } from "@utils/hooks/useSocial";
|
import { Image } from '@shared/image';
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
import { Link, useParams } from "react-router-dom";
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { useSocial } from '@utils/hooks/useSocial';
|
||||||
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function UserScreen() {
|
export function UserScreen() {
|
||||||
const { pubkey } = useParams();
|
const { pubkey } = useParams();
|
||||||
@@ -43,7 +47,7 @@ export function UserScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "success" && userFollows) {
|
if (status === 'success' && userFollows) {
|
||||||
if (userFollows.includes(pubkey)) {
|
if (userFollows.includes(pubkey)) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
}
|
}
|
||||||
@@ -54,39 +58,39 @@ export function UserScreen() {
|
|||||||
<div className="h-full w-full overflow-y-auto">
|
<div className="h-full w-full overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-11 w-full flex items-center px-3 border-b border-zinc-900"
|
className="flex h-11 w-full items-center border-b border-zinc-900 px-3"
|
||||||
/>
|
/>
|
||||||
<div className="w-full h-56 bg-zinc-100">
|
<div className="h-56 w-full bg-zinc-100">
|
||||||
<Image
|
<Image
|
||||||
src={user?.banner}
|
src={user?.banner}
|
||||||
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
|
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
|
||||||
alt={"banner"}
|
alt={'banner'}
|
||||||
className="w-full h-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full -mt-7">
|
<div className="-mt-7 w-full">
|
||||||
<div className="px-5">
|
<div className="px-5">
|
||||||
<Image
|
<Image
|
||||||
src={user?.image}
|
src={user?.image}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="w-14 h-14 rounded-md ring-2 ring-black"
|
className="h-14 w-14 rounded-md ring-2 ring-black"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 flex flex-col gap-4 mt-2">
|
<div className="mt-2 flex flex-1 flex-col gap-4">
|
||||||
<div className="flex items-center gap-16">
|
<div className="flex items-center gap-16">
|
||||||
<div className="inline-flex flex-col gap-1.5">
|
<div className="inline-flex flex-col gap-1.5">
|
||||||
<h5 className="font-semibold text-lg leading-none">
|
<h5 className="text-lg font-semibold leading-none">
|
||||||
{user?.displayName || user?.name || "No name"}
|
{user?.displayName || user?.name || 'No name'}
|
||||||
</h5>
|
</h5>
|
||||||
<span className="max-w-[15rem] text-sm truncate leading-none text-zinc-500">
|
<span className="max-w-[15rem] truncate text-sm leading-none text-zinc-500">
|
||||||
{user?.nip05 || shortenKey(pubkey)}
|
{user?.nip05 || shortenKey(pubkey)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
{status === "loading" ? (
|
{status === 'loading' ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
Loading...
|
Loading...
|
||||||
</button>
|
</button>
|
||||||
@@ -94,7 +98,7 @@ export function UserScreen() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => unfollowUser(pubkey)}
|
onClick={() => unfollowUser(pubkey)}
|
||||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
Unfollow
|
Unfollow
|
||||||
</button>
|
</button>
|
||||||
@@ -102,29 +106,29 @@ export function UserScreen() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => followUser(pubkey)}
|
onClick={() => followUser(pubkey)}
|
||||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
Follow
|
Follow
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to={`/app/chat/${pubkey}`}
|
to={`/app/chat/${pubkey}`}
|
||||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex w-10 h-10 items-center justify-center rounded-md bg-zinc-900 group hover:bg-orange-500 text-sm font-medium"
|
className="group inline-flex h-10 w-10 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-orange-500"
|
||||||
>
|
>
|
||||||
<ZapIcon className="w-5 h-5" />
|
<ZapIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<span className="inline-flex mx-2 w-px h-4 bg-zinc-900" />
|
<span className="mx-2 inline-flex h-4 w-px bg-zinc-900" />
|
||||||
{account && account.pubkey === pubkey && <EditProfileModal />}
|
{account && account.pubkey === pubkey && <EditProfileModal />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<p className="mt-2 max-w-[500px] break-words select-text text-zinc-100">
|
<p className="mt-2 max-w-[500px] select-text break-words text-zinc-100">
|
||||||
{user?.about}
|
{user?.about}
|
||||||
</p>
|
</p>
|
||||||
<UserMetadata pubkey={pubkey} />
|
<UserMetadata pubkey={pubkey} />
|
||||||
@@ -133,18 +137,18 @@ export function UserScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-8 w-full border-t border-zinc-900">
|
<div className="mt-8 w-full border-t border-zinc-900">
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="px-5 mb-2">
|
<Tab.List className="mb-2 px-5">
|
||||||
<Tab as={Fragment}>
|
<Tab as={Fragment}>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${
|
className={`${
|
||||||
selected
|
selected
|
||||||
? "text-fuchsia-500 border-fuchsia-500"
|
? 'border-fuchsia-500 text-fuchsia-500'
|
||||||
: "text-zinc-200 border-transparent"
|
: 'border-transparent text-zinc-200'
|
||||||
} font-medium inline-flex items-center gap-2 h-10 border-t`}
|
} inline-flex h-10 items-center gap-2 border-t font-medium`}
|
||||||
>
|
>
|
||||||
<ThreadsIcon className="w-4 h-4" />
|
<ThreadsIcon className="h-4 w-4" />
|
||||||
Activities from 48 hours ago
|
Activities from 48 hours ago
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown {
|
.markdown {
|
||||||
@apply prose prose-zinc max-w-none select-text break-words dark:prose-invert prose-p:mt-0 prose-p:mb-2 prose-p:last:mb-0 prose-p:leading-tight prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 hover:prose-a:text-fuchsia-600 prose-ol:mb-1 prose-ul:mb-1 prose-li:leading-tight prose-blockquote:m-0 prose-ol:m-0 prose-hr:mx-0 prose-hr:my-2;
|
@apply prose prose-zinc max-w-none select-text break-words dark:prose-invert prose-p:mb-2 prose-p:mt-0 prose-p:leading-tight prose-p:last:mb-0 prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 hover:prose-a:text-fuchsia-600 prose-blockquote:m-0 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-li:leading-tight prose-hr:mx-0 prose-hr:my-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import NDK, {
|
|||||||
NDKFilter,
|
NDKFilter,
|
||||||
NDKKind,
|
NDKKind,
|
||||||
NDKPrivateKeySigner,
|
NDKPrivateKeySigner,
|
||||||
} from "@nostr-dev-kit/ndk";
|
} from '@nostr-dev-kit/ndk';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { useContext } from 'react';
|
||||||
import { FULL_RELAYS } from "@stores/constants";
|
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { useContext } from "react";
|
|
||||||
|
import { FULL_RELAYS } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export async function initNDK(relays?: string[]): Promise<NDK> {
|
export async function initNDK(relays?: string[]): Promise<NDK> {
|
||||||
const opts: NDKConstructorParams = {};
|
const opts: NDKConstructorParams = {};
|
||||||
@@ -24,7 +27,7 @@ export async function initNDK(relays?: string[]): Promise<NDK> {
|
|||||||
|
|
||||||
export async function prefetchEvents(
|
export async function prefetchEvents(
|
||||||
ndk: NDK,
|
ndk: NDK,
|
||||||
filter: NDKFilter,
|
filter: NDKFilter
|
||||||
): Promise<Set<NDKEvent>> {
|
): Promise<Set<NDKEvent>> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const events: Map<string, NDKEvent> = new Map();
|
const events: Map<string, NDKEvent> = new Map();
|
||||||
@@ -33,12 +36,12 @@ export async function prefetchEvents(
|
|||||||
closeOnEose: true,
|
closeOnEose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
relaySetSubscription.on("event", (event: NDKEvent) => {
|
relaySetSubscription.on('event', (event: NDKEvent) => {
|
||||||
event.ndk = ndk;
|
event.ndk = ndk;
|
||||||
events.set(event.tagId(), event);
|
events.set(event.tagId(), event);
|
||||||
});
|
});
|
||||||
|
|
||||||
relaySetSubscription.on("eose", () => {
|
relaySetSubscription.on('eose', () => {
|
||||||
setTimeout(() => resolve(new Set(events.values())), 3000);
|
setTimeout(() => resolve(new Set(events.values())), 3000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { OPENGRAPH } from "@stores/constants";
|
import { FetchOptions, ResponseType, fetch } from '@tauri-apps/api/http';
|
||||||
import { FetchOptions, ResponseType, fetch } from "@tauri-apps/api/http";
|
import * as cheerio from 'cheerio';
|
||||||
import * as cheerio from "cheerio";
|
|
||||||
|
import { OPENGRAPH } from '@stores/constants';
|
||||||
|
|
||||||
interface ILinkPreviewOptions {
|
interface ILinkPreviewOptions {
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
@@ -27,64 +28,63 @@ function metaTag(doc: cheerio.CheerioAPI, type: string, attr: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function metaTagContent(doc: cheerio.CheerioAPI, type: string, attr: string) {
|
function metaTagContent(doc: cheerio.CheerioAPI, type: string, attr: string) {
|
||||||
return doc(`meta[${attr}='${type}']`).attr("content");
|
return doc(`meta[${attr}='${type}']`).attr('content');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTitle(doc: cheerio.CheerioAPI) {
|
function getTitle(doc: cheerio.CheerioAPI) {
|
||||||
let title =
|
let title =
|
||||||
metaTagContent(doc, "og:title", "property") ||
|
metaTagContent(doc, 'og:title', 'property') ||
|
||||||
metaTagContent(doc, "og:title", "name");
|
metaTagContent(doc, 'og:title', 'name');
|
||||||
if (!title) {
|
if (!title) {
|
||||||
title = doc("title").text();
|
title = doc('title').text();
|
||||||
}
|
}
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSiteName(doc: cheerio.CheerioAPI) {
|
function getSiteName(doc: cheerio.CheerioAPI) {
|
||||||
const siteName =
|
const siteName =
|
||||||
metaTagContent(doc, "og:site_name", "property") ||
|
metaTagContent(doc, 'og:site_name', 'property') ||
|
||||||
metaTagContent(doc, "og:site_name", "name");
|
metaTagContent(doc, 'og:site_name', 'name');
|
||||||
return siteName;
|
return siteName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDescription(doc: cheerio.CheerioAPI) {
|
function getDescription(doc: cheerio.CheerioAPI) {
|
||||||
const description =
|
const description =
|
||||||
metaTagContent(doc, "description", "name") ||
|
metaTagContent(doc, 'description', 'name') ||
|
||||||
metaTagContent(doc, "Description", "name") ||
|
metaTagContent(doc, 'Description', 'name') ||
|
||||||
metaTagContent(doc, "og:description", "property");
|
metaTagContent(doc, 'og:description', 'property');
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMediaType(doc: cheerio.CheerioAPI) {
|
function getMediaType(doc: cheerio.CheerioAPI) {
|
||||||
const node = metaTag(doc, "medium", "name");
|
const node = metaTag(doc, 'medium', 'name');
|
||||||
if (node) {
|
if (node) {
|
||||||
const content = node.attr("content");
|
const content = node.attr('content');
|
||||||
return content === "image" ? "photo" : content;
|
return content === 'image' ? 'photo' : content;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
metaTagContent(doc, "og:type", "property") ||
|
metaTagContent(doc, 'og:type', 'property') || metaTagContent(doc, 'og:type', 'name')
|
||||||
metaTagContent(doc, "og:type", "name")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImages(
|
function getImages(
|
||||||
doc: cheerio.CheerioAPI,
|
doc: cheerio.CheerioAPI,
|
||||||
rootUrl: string,
|
rootUrl: string,
|
||||||
imagesPropertyType?: string,
|
imagesPropertyType?: string
|
||||||
) {
|
) {
|
||||||
let images: string[] = [];
|
let images: string[] = [];
|
||||||
let nodes: cheerio.Cheerio<cheerio.Element> | null;
|
let nodes: cheerio.Cheerio<cheerio.Element> | null;
|
||||||
let src: string | undefined;
|
let src: string | undefined;
|
||||||
let dic: Record<string, boolean> = {};
|
let dic: Record<string, boolean> = {};
|
||||||
|
|
||||||
const imagePropertyType = imagesPropertyType ?? "og";
|
const imagePropertyType = imagesPropertyType ?? 'og';
|
||||||
nodes =
|
nodes =
|
||||||
metaTag(doc, `${imagePropertyType}:image`, "property") ||
|
metaTag(doc, `${imagePropertyType}:image`, 'property') ||
|
||||||
metaTag(doc, `${imagePropertyType}:image`, "name");
|
metaTag(doc, `${imagePropertyType}:image`, 'name');
|
||||||
|
|
||||||
if (nodes) {
|
if (nodes) {
|
||||||
nodes.each((_: number, node: cheerio.Element) => {
|
nodes.each((_: number, node: cheerio.Element) => {
|
||||||
if (node.type === "tag") {
|
if (node.type === 'tag') {
|
||||||
src = node.attribs.content;
|
src = node.attribs.content;
|
||||||
if (src) {
|
if (src) {
|
||||||
src = new URL(src, rootUrl).href;
|
src = new URL(src, rootUrl).href;
|
||||||
@@ -95,18 +95,18 @@ function getImages(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (images.length <= 0 && !imagesPropertyType) {
|
if (images.length <= 0 && !imagesPropertyType) {
|
||||||
src = doc("link[rel=image_src]").attr("href");
|
src = doc('link[rel=image_src]').attr('href');
|
||||||
if (src) {
|
if (src) {
|
||||||
src = new URL(src, rootUrl).href;
|
src = new URL(src, rootUrl).href;
|
||||||
images = [src];
|
images = [src];
|
||||||
} else {
|
} else {
|
||||||
nodes = doc("img");
|
nodes = doc('img');
|
||||||
|
|
||||||
if (nodes?.length) {
|
if (nodes?.length) {
|
||||||
dic = {};
|
dic = {};
|
||||||
images = [];
|
images = [];
|
||||||
nodes.each((_: number, node: cheerio.Element) => {
|
nodes.each((_: number, node: cheerio.Element) => {
|
||||||
if (node.type === "tag") src = node.attribs.src;
|
if (node.type === 'tag') src = node.attribs.src;
|
||||||
if (src && !dic[src]) {
|
if (src && !dic[src]) {
|
||||||
dic[src] = true;
|
dic[src] = true;
|
||||||
// width = node.attribs.width;
|
// width = node.attribs.width;
|
||||||
@@ -135,34 +135,32 @@ function getVideos(doc: cheerio.CheerioAPI) {
|
|||||||
let videoObj;
|
let videoObj;
|
||||||
let index;
|
let index;
|
||||||
|
|
||||||
const nodes =
|
const nodes = metaTag(doc, 'og:video', 'property') || metaTag(doc, 'og:video', 'name');
|
||||||
metaTag(doc, "og:video", "property") || metaTag(doc, "og:video", "name");
|
|
||||||
|
|
||||||
if (nodes?.length) {
|
if (nodes?.length) {
|
||||||
nodeTypes =
|
nodeTypes =
|
||||||
metaTag(doc, "og:video:type", "property") ||
|
metaTag(doc, 'og:video:type', 'property') || metaTag(doc, 'og:video:type', 'name');
|
||||||
metaTag(doc, "og:video:type", "name");
|
|
||||||
nodeSecureUrls =
|
nodeSecureUrls =
|
||||||
metaTag(doc, "og:video:secure_url", "property") ||
|
metaTag(doc, 'og:video:secure_url', 'property') ||
|
||||||
metaTag(doc, "og:video:secure_url", "name");
|
metaTag(doc, 'og:video:secure_url', 'name');
|
||||||
width =
|
width =
|
||||||
metaTagContent(doc, "og:video:width", "property") ||
|
metaTagContent(doc, 'og:video:width', 'property') ||
|
||||||
metaTagContent(doc, "og:video:width", "name");
|
metaTagContent(doc, 'og:video:width', 'name');
|
||||||
height =
|
height =
|
||||||
metaTagContent(doc, "og:video:height", "property") ||
|
metaTagContent(doc, 'og:video:height', 'property') ||
|
||||||
metaTagContent(doc, "og:video:height", "name");
|
metaTagContent(doc, 'og:video:height', 'name');
|
||||||
|
|
||||||
for (index = 0; index < nodes.length; index += 1) {
|
for (index = 0; index < nodes.length; index += 1) {
|
||||||
const node = nodes[index];
|
const node = nodes[index];
|
||||||
if (node.type === "tag") video = node.attribs.content;
|
if (node.type === 'tag') video = node.attribs.content;
|
||||||
|
|
||||||
nodeType = nodeTypes?.[index];
|
nodeType = nodeTypes?.[index];
|
||||||
if (nodeType?.type === "tag") {
|
if (nodeType?.type === 'tag') {
|
||||||
videoType = nodeType ? nodeType.attribs.content : null;
|
videoType = nodeType ? nodeType.attribs.content : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeSecureUrl = nodeSecureUrls?.[index];
|
nodeSecureUrl = nodeSecureUrls?.[index];
|
||||||
if (nodeSecureUrl?.type === "tag") {
|
if (nodeSecureUrl?.type === 'tag') {
|
||||||
videoSecureUrl = nodeSecureUrl ? nodeSecureUrl.attribs.content : null;
|
videoSecureUrl = nodeSecureUrl ? nodeSecureUrl.attribs.content : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +171,7 @@ function getVideos(doc: cheerio.CheerioAPI) {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
};
|
};
|
||||||
if (videoType && videoType.indexOf("video/") === 0) {
|
if (videoType && videoType.indexOf('video/') === 0) {
|
||||||
videos.splice(0, 0, videoObj);
|
videos.splice(0, 0, videoObj);
|
||||||
} else {
|
} else {
|
||||||
videos.push(videoObj);
|
videos.push(videoObj);
|
||||||
@@ -195,11 +193,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
|
|||||||
let nodes: cheerio.Cheerio<cheerio.Element> | never[] = [];
|
let nodes: cheerio.Cheerio<cheerio.Element> | never[] = [];
|
||||||
let src: string | undefined;
|
let src: string | undefined;
|
||||||
|
|
||||||
const relSelectors = [
|
const relSelectors = ['rel=icon', `rel="shortcut icon"`, 'rel=apple-touch-icon'];
|
||||||
"rel=icon",
|
|
||||||
`rel="shortcut icon"`,
|
|
||||||
"rel=apple-touch-icon",
|
|
||||||
];
|
|
||||||
|
|
||||||
relSelectors.forEach((relSelector) => {
|
relSelectors.forEach((relSelector) => {
|
||||||
// look for all icon tags
|
// look for all icon tags
|
||||||
@@ -208,7 +202,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
|
|||||||
// collect all images from icon tags
|
// collect all images from icon tags
|
||||||
if (nodes.length) {
|
if (nodes.length) {
|
||||||
nodes.each((_: number, node: cheerio.Element) => {
|
nodes.each((_: number, node: cheerio.Element) => {
|
||||||
if (node.type === "tag") src = node.attribs.href;
|
if (node.type === 'tag') src = node.attribs.href;
|
||||||
if (src) {
|
if (src) {
|
||||||
src = new URL(rootUrl).href;
|
src = new URL(rootUrl).href;
|
||||||
images.push(src);
|
images.push(src);
|
||||||
@@ -228,7 +222,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
|
|||||||
function parseImageResponse(url: string, contentType: string) {
|
function parseImageResponse(url: string, contentType: string) {
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
mediaType: "image",
|
mediaType: 'image',
|
||||||
contentType,
|
contentType,
|
||||||
favicons: [getDefaultFavicon(url)],
|
favicons: [getDefaultFavicon(url)],
|
||||||
};
|
};
|
||||||
@@ -237,7 +231,7 @@ function parseImageResponse(url: string, contentType: string) {
|
|||||||
function parseAudioResponse(url: string, contentType: string) {
|
function parseAudioResponse(url: string, contentType: string) {
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
mediaType: "audio",
|
mediaType: 'audio',
|
||||||
contentType,
|
contentType,
|
||||||
favicons: [getDefaultFavicon(url)],
|
favicons: [getDefaultFavicon(url)],
|
||||||
};
|
};
|
||||||
@@ -246,7 +240,7 @@ function parseAudioResponse(url: string, contentType: string) {
|
|||||||
function parseVideoResponse(url: string, contentType: string) {
|
function parseVideoResponse(url: string, contentType: string) {
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
mediaType: "video",
|
mediaType: 'video',
|
||||||
contentType,
|
contentType,
|
||||||
favicons: [getDefaultFavicon(url)],
|
favicons: [getDefaultFavicon(url)],
|
||||||
};
|
};
|
||||||
@@ -255,7 +249,7 @@ function parseVideoResponse(url: string, contentType: string) {
|
|||||||
function parseApplicationResponse(url: string, contentType: string) {
|
function parseApplicationResponse(url: string, contentType: string) {
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
mediaType: "application",
|
mediaType: 'application',
|
||||||
contentType,
|
contentType,
|
||||||
favicons: [getDefaultFavicon(url)],
|
favicons: [getDefaultFavicon(url)],
|
||||||
};
|
};
|
||||||
@@ -265,7 +259,7 @@ function parseTextResponse(
|
|||||||
body: string,
|
body: string,
|
||||||
url: string,
|
url: string,
|
||||||
options: ILinkPreviewOptions = {},
|
options: ILinkPreviewOptions = {},
|
||||||
contentType?: string,
|
contentType?: string
|
||||||
) {
|
) {
|
||||||
const doc = cheerio.load(body);
|
const doc = cheerio.load(body);
|
||||||
|
|
||||||
@@ -274,7 +268,7 @@ function parseTextResponse(
|
|||||||
title: getTitle(doc),
|
title: getTitle(doc),
|
||||||
siteName: getSiteName(doc),
|
siteName: getSiteName(doc),
|
||||||
description: getDescription(doc),
|
description: getDescription(doc),
|
||||||
mediaType: getMediaType(doc) || "website",
|
mediaType: getMediaType(doc) || 'website',
|
||||||
contentType,
|
contentType,
|
||||||
images: getImages(doc, url, options.imagesPropertyType),
|
images: getImages(doc, url, options.imagesPropertyType),
|
||||||
videos: getVideos(doc),
|
videos: getVideos(doc),
|
||||||
@@ -286,21 +280,18 @@ function parseUnknownResponse(
|
|||||||
body: string,
|
body: string,
|
||||||
url: string,
|
url: string,
|
||||||
options: ILinkPreviewOptions = {},
|
options: ILinkPreviewOptions = {},
|
||||||
contentType?: string,
|
contentType?: string
|
||||||
) {
|
) {
|
||||||
return parseTextResponse(body, url, options, contentType);
|
return parseTextResponse(body, url, options, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseResponse(
|
function parseResponse(response: IPreFetchedResource, options?: ILinkPreviewOptions) {
|
||||||
response: IPreFetchedResource,
|
|
||||||
options?: ILinkPreviewOptions,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
let contentType = response.headers["content-type"];
|
let contentType = response.headers['content-type'];
|
||||||
// console.warn(`original content type`, contentType);
|
// console.warn(`original content type`, contentType);
|
||||||
if (contentType?.indexOf(";")) {
|
if (contentType?.indexOf(';')) {
|
||||||
// eslint-disable-next-line prefer-destructuring
|
// eslint-disable-next-line prefer-destructuring
|
||||||
contentType = contentType.split(";")[0];
|
contentType = contentType.split(';')[0];
|
||||||
// console.warn(`splitting content type`, contentType);
|
// console.warn(`splitting content type`, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,9 +325,7 @@ function parseResponse(
|
|||||||
return parseUnknownResponse(htmlString, response.url, options);
|
return parseUnknownResponse(htmlString, response.url, options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`link-preview-js could not fetch link information ${(
|
`link-preview-js could not fetch link information ${(e as any).toString()}`
|
||||||
e as any
|
|
||||||
).toString()}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,7 +333,7 @@ function parseResponse(
|
|||||||
export async function getLinkPreview(text: string) {
|
export async function getLinkPreview(text: string) {
|
||||||
const fetchUrl = text;
|
const fetchUrl = text;
|
||||||
const options: FetchOptions = {
|
const options: FetchOptions = {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
responseType: ResponseType.Text,
|
responseType: ResponseType.Text,
|
||||||
};
|
};
|
||||||
@@ -352,7 +341,7 @@ export async function getLinkPreview(text: string) {
|
|||||||
let response = await fetch(fetchUrl, options);
|
let response = await fetch(fetchUrl, options);
|
||||||
|
|
||||||
if (response.status > 300 && response.status < 309) {
|
if (response.status > 300 && response.status < 309) {
|
||||||
const forwardedUrl = response.headers.location || "";
|
const forwardedUrl = response.headers.location || '';
|
||||||
response = await fetch(forwardedUrl, options);
|
response = await fetch(forwardedUrl, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getParentID } from "@utils/transform";
|
import Database from 'tauri-plugin-sql-api';
|
||||||
import Database from "tauri-plugin-sql-api";
|
|
||||||
|
import { getParentID } from '@utils/transform';
|
||||||
|
|
||||||
let db: null | Database = null;
|
let db: null | Database = null;
|
||||||
|
|
||||||
@@ -9,16 +10,14 @@ export async function connect(): Promise<Database> {
|
|||||||
if (db) {
|
if (db) {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
db = await Database.load("sqlite:lume.db");
|
db = await Database.load('sqlite:lume.db');
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get active account
|
// get active account
|
||||||
export async function getActiveAccount() {
|
export async function getActiveAccount() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result: any = await db.select(
|
const result: any = await db.select('SELECT * FROM accounts WHERE is_active = 1;');
|
||||||
"SELECT * FROM accounts WHERE is_active = 1;",
|
|
||||||
);
|
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
return result[0];
|
return result[0];
|
||||||
} else {
|
} else {
|
||||||
@@ -30,7 +29,7 @@ export async function getActiveAccount() {
|
|||||||
export async function getAccounts() {
|
export async function getAccounts() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select(
|
return await db.select(
|
||||||
"SELECT * FROM accounts WHERE is_active = 0 ORDER BY created_at DESC;",
|
'SELECT * FROM accounts WHERE is_active = 0 ORDER BY created_at DESC;'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,18 +39,18 @@ export async function createAccount(
|
|||||||
pubkey: string,
|
pubkey: string,
|
||||||
privkey: string,
|
privkey: string,
|
||||||
follows?: string[][],
|
follows?: string[][],
|
||||||
is_active?: number,
|
is_active?: number
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const res = await db.execute(
|
const res = await db.execute(
|
||||||
"INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);",
|
'INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);',
|
||||||
[npub, pubkey, privkey, follows || "", is_active || 0],
|
[npub, pubkey, privkey, follows || '', is_active || 0]
|
||||||
);
|
);
|
||||||
if (res) {
|
if (res) {
|
||||||
await createBlock(
|
await createBlock(
|
||||||
0,
|
0,
|
||||||
"Preserve your freedom",
|
'Preserve your freedom',
|
||||||
"https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv",
|
'https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const getAccount = await getActiveAccount();
|
const getAccount = await getActiveAccount();
|
||||||
@@ -62,13 +61,13 @@ export async function createAccount(
|
|||||||
export async function updateAccount(
|
export async function updateAccount(
|
||||||
column: string,
|
column: string,
|
||||||
value: string | string[],
|
value: string | string[],
|
||||||
pubkey: string,
|
pubkey: string
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(`UPDATE accounts SET ${column} = ? WHERE pubkey = ?;`, [
|
||||||
`UPDATE accounts SET ${column} = ? WHERE pubkey = ?;`,
|
value,
|
||||||
[value, pubkey],
|
pubkey,
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// count total notes
|
// count total notes
|
||||||
@@ -82,7 +81,7 @@ export async function countTotalChannels() {
|
|||||||
export async function countTotalNotes() {
|
export async function countTotalNotes() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result = await db.select(
|
const result = await db.select(
|
||||||
'SELECT COUNT(*) AS "total" FROM notes WHERE kind IN (1, 6);',
|
'SELECT COUNT(*) AS "total" FROM notes WHERE kind IN (1, 6);'
|
||||||
);
|
);
|
||||||
return parseInt(result[0].total);
|
return parseInt(result[0].total);
|
||||||
}
|
}
|
||||||
@@ -95,12 +94,11 @@ export async function getNotes(limit: number, offset: number) {
|
|||||||
|
|
||||||
const notes: any = { data: null, nextCursor: 0 };
|
const notes: any = { data: null, nextCursor: 0 };
|
||||||
const query: any = await db.select(
|
const query: any = await db.select(
|
||||||
`SELECT * FROM notes WHERE kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`,
|
`SELECT * FROM notes WHERE kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`
|
||||||
);
|
);
|
||||||
|
|
||||||
notes["data"] = query;
|
notes['data'] = query;
|
||||||
notes["nextCursor"] =
|
notes['nextCursor'] = Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
|
||||||
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
|
|
||||||
|
|
||||||
return notes;
|
return notes;
|
||||||
}
|
}
|
||||||
@@ -109,18 +107,14 @@ export async function getNotes(limit: number, offset: number) {
|
|||||||
export async function getNotesByPubkey(pubkey: string) {
|
export async function getNotesByPubkey(pubkey: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const res: any = await db.select(
|
const res: any = await db.select(
|
||||||
`SELECT * FROM notes WHERE pubkey == "${pubkey}" AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC;`,
|
`SELECT * FROM notes WHERE pubkey == "${pubkey}" AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC;`
|
||||||
);
|
);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all notes by authors
|
// get all notes by authors
|
||||||
export async function getNotesByAuthors(
|
export async function getNotesByAuthors(authors: string, limit: number, offset: number) {
|
||||||
authors: string,
|
|
||||||
limit: number,
|
|
||||||
offset: number,
|
|
||||||
) {
|
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const totalNotes = await countTotalNotes();
|
const totalNotes = await countTotalNotes();
|
||||||
const nextCursor = offset + limit;
|
const nextCursor = offset + limit;
|
||||||
@@ -129,12 +123,11 @@ export async function getNotesByAuthors(
|
|||||||
|
|
||||||
const notes: any = { data: null, nextCursor: 0 };
|
const notes: any = { data: null, nextCursor: 0 };
|
||||||
const query: any = await db.select(
|
const query: any = await db.select(
|
||||||
`SELECT * FROM notes WHERE pubkey IN (${finalArray}) AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`,
|
`SELECT * FROM notes WHERE pubkey IN (${finalArray}) AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`
|
||||||
);
|
);
|
||||||
|
|
||||||
notes["data"] = query;
|
notes['data'] = query;
|
||||||
notes["nextCursor"] =
|
notes['nextCursor'] = Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
|
||||||
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
|
|
||||||
|
|
||||||
return notes;
|
return notes;
|
||||||
}
|
}
|
||||||
@@ -142,9 +135,7 @@ export async function getNotesByAuthors(
|
|||||||
// get note by id
|
// get note by id
|
||||||
export async function getNoteByID(event_id: string) {
|
export async function getNoteByID(event_id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result = await db.select(
|
const result = await db.select(`SELECT * FROM notes WHERE event_id = "${event_id}";`);
|
||||||
`SELECT * FROM notes WHERE event_id = "${event_id}";`,
|
|
||||||
);
|
|
||||||
return result[0];
|
return result[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,15 +146,15 @@ export async function createNote(
|
|||||||
kind: number,
|
kind: number,
|
||||||
tags: any,
|
tags: any,
|
||||||
content: string,
|
content: string,
|
||||||
created_at: number,
|
created_at: number
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const account = await getActiveAccount();
|
const account = await getActiveAccount();
|
||||||
const parentID = getParentID(tags, event_id);
|
const parentID = getParentID(tags, event_id);
|
||||||
|
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"INSERT OR IGNORE INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
|
'INSERT OR IGNORE INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);',
|
||||||
[event_id, account.id, pubkey, kind, tags, content, created_at, parentID],
|
[event_id, account.id, pubkey, kind, tags, content, created_at, parentID]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +162,7 @@ export async function createNote(
|
|||||||
export async function getReplies(parent_id: string) {
|
export async function getReplies(parent_id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result: any = await db.select(
|
const result: any = await db.select(
|
||||||
`SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`,
|
`SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -184,30 +175,26 @@ export async function createReplyNote(
|
|||||||
kind: number,
|
kind: number,
|
||||||
tags: any,
|
tags: any,
|
||||||
content: string,
|
content: string,
|
||||||
created_at: number,
|
created_at: number
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"INSERT OR IGNORE INTO replies (event_id, parent_id, pubkey, kind, tags, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
'INSERT OR IGNORE INTO replies (event_id, parent_id, pubkey, kind, tags, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);',
|
||||||
[event_id, parent_id, pubkey, kind, tags, content, created_at],
|
[event_id, parent_id, pubkey, kind, tags, content, created_at]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all channels
|
// get all channels
|
||||||
export async function getChannels() {
|
export async function getChannels() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result: any = await db.select(
|
const result: any = await db.select('SELECT * FROM channels ORDER BY created_at DESC;');
|
||||||
"SELECT * FROM channels ORDER BY created_at DESC;",
|
|
||||||
);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get channel by id
|
// get channel by id
|
||||||
export async function getChannel(id: string) {
|
export async function getChannel(id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result = await db.select(
|
const result = await db.select(`SELECT * FROM channels WHERE event_id = "${id}";`);
|
||||||
`SELECT * FROM channels WHERE event_id = "${id}";`,
|
|
||||||
);
|
|
||||||
return result[0];
|
return result[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,12 +205,12 @@ export async function createChannel(
|
|||||||
name: string,
|
name: string,
|
||||||
picture: string,
|
picture: string,
|
||||||
about: string,
|
about: string,
|
||||||
created_at: number,
|
created_at: number
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"INSERT OR IGNORE INTO channels (event_id, pubkey, name, picture, about, created_at) VALUES (?, ?, ?, ?, ?, ?);",
|
'INSERT OR IGNORE INTO channels (event_id, pubkey, name, picture, about, created_at) VALUES (?, ?, ?, ?, ?, ?);',
|
||||||
[event_id, pubkey, name, picture, about, created_at],
|
[event_id, pubkey, name, picture, about, created_at]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,8 +220,8 @@ export async function updateChannelMetadata(event_id: string, value: string) {
|
|||||||
const data = JSON.parse(value);
|
const data = JSON.parse(value);
|
||||||
|
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;",
|
'UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;',
|
||||||
[data.name, data.picture, data.about, event_id],
|
[data.name, data.picture, data.about, event_id]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,12 +233,12 @@ export async function createChannelMessage(
|
|||||||
kind: number,
|
kind: number,
|
||||||
content: string,
|
content: string,
|
||||||
tags: string[][],
|
tags: string[][],
|
||||||
created_at: number,
|
created_at: number
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"INSERT OR IGNORE INTO channel_messages (channel_id, event_id, pubkey, kind, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
'INSERT OR IGNORE INTO channel_messages (channel_id, event_id, pubkey, kind, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);',
|
||||||
[channel_id, event_id, pubkey, kind, content, tags, created_at],
|
[channel_id, event_id, pubkey, kind, content, tags, created_at]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +246,7 @@ export async function createChannelMessage(
|
|||||||
export async function getChannelMessages(channel_id: string) {
|
export async function getChannelMessages(channel_id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select(
|
return await db.select(
|
||||||
`SELECT * FROM channel_messages WHERE channel_id = "${channel_id}" ORDER BY created_at ASC;`,
|
`SELECT * FROM channel_messages WHERE channel_id = "${channel_id}" ORDER BY created_at ASC;`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +254,7 @@ export async function getChannelMessages(channel_id: string) {
|
|||||||
export async function getChannelUsers(channel_id: string) {
|
export async function getChannelUsers(channel_id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result: any = await db.select(
|
const result: any = await db.select(
|
||||||
`SELECT DISTINCT pubkey FROM channel_messages WHERE channel_id = "${channel_id}";`,
|
`SELECT DISTINCT pubkey FROM channel_messages WHERE channel_id = "${channel_id}";`
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -276,33 +263,29 @@ export async function getChannelUsers(channel_id: string) {
|
|||||||
export async function getChatsByPubkey(pubkey: string) {
|
export async function getChatsByPubkey(pubkey: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result: any = await db.select(
|
const result: any = await db.select(
|
||||||
`SELECT DISTINCT sender_pubkey FROM chats WHERE receiver_pubkey = "${pubkey}" ORDER BY created_at DESC;`,
|
`SELECT DISTINCT sender_pubkey FROM chats WHERE receiver_pubkey = "${pubkey}" ORDER BY created_at DESC;`
|
||||||
);
|
);
|
||||||
const newArr: any = result.map((v) => ({ ...v, new_messages: 0 }));
|
const newArr: any = result.map((v) => ({ ...v, new_messages: 0 }));
|
||||||
return newArr;
|
return newArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get chat messages
|
// get chat messages
|
||||||
export async function getChatMessages(
|
export async function getChatMessages(receiver_pubkey: string, sender_pubkey: string) {
|
||||||
receiver_pubkey: string,
|
|
||||||
sender_pubkey: string,
|
|
||||||
) {
|
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
let receiver = [];
|
let receiver = [];
|
||||||
|
|
||||||
const sender: any = await db.select(
|
const sender: any = await db.select(
|
||||||
`SELECT * FROM chats WHERE sender_pubkey = "${sender_pubkey}" AND receiver_pubkey = "${receiver_pubkey}";`,
|
`SELECT * FROM chats WHERE sender_pubkey = "${sender_pubkey}" AND receiver_pubkey = "${receiver_pubkey}";`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (receiver_pubkey !== sender_pubkey) {
|
if (receiver_pubkey !== sender_pubkey) {
|
||||||
receiver = await db.select(
|
receiver = await db.select(
|
||||||
`SELECT * FROM chats WHERE sender_pubkey = "${receiver_pubkey}" AND receiver_pubkey = "${sender_pubkey}";`,
|
`SELECT * FROM chats WHERE sender_pubkey = "${receiver_pubkey}" AND receiver_pubkey = "${sender_pubkey}";`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = [...sender, ...receiver].sort(
|
const result = [...sender, ...receiver].sort(
|
||||||
(x: { created_at: number }, y: { created_at: number }) =>
|
(x: { created_at: number }, y: { created_at: number }) => x.created_at - y.created_at
|
||||||
x.created_at - y.created_at,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -315,12 +298,12 @@ export async function createChat(
|
|||||||
sender_pubkey: string,
|
sender_pubkey: string,
|
||||||
content: string,
|
content: string,
|
||||||
tags: string[][],
|
tags: string[][],
|
||||||
created_at: number,
|
created_at: number
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT OR IGNORE INTO chats (event_id, receiver_pubkey, sender_pubkey, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?);",
|
'INSERT OR IGNORE INTO chats (event_id, receiver_pubkey, sender_pubkey, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?);',
|
||||||
[event_id, receiver_pubkey, sender_pubkey, content, tags, created_at],
|
[event_id, receiver_pubkey, sender_pubkey, content, tags, created_at]
|
||||||
);
|
);
|
||||||
return sender_pubkey;
|
return sender_pubkey;
|
||||||
}
|
}
|
||||||
@@ -328,26 +311,20 @@ export async function createChat(
|
|||||||
// get setting
|
// get setting
|
||||||
export async function getSetting(key: string) {
|
export async function getSetting(key: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result = await db.select(
|
const result = await db.select(`SELECT value FROM settings WHERE key = "${key}";`);
|
||||||
`SELECT value FROM settings WHERE key = "${key}";`,
|
|
||||||
);
|
|
||||||
return result[0]?.value;
|
return result[0]?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update setting
|
// update setting
|
||||||
export async function updateSetting(key: string, value: string | number) {
|
export async function updateSetting(key: string, value: string | number) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(`UPDATE settings SET value = "${value}" WHERE key = "${key}";`);
|
||||||
`UPDATE settings SET value = "${value}" WHERE key = "${key}";`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get last login
|
// get last login
|
||||||
export async function getLastLogin() {
|
export async function getLastLogin() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result = await db.select(
|
const result = await db.select(`SELECT value FROM settings WHERE key = "last_login";`);
|
||||||
`SELECT value FROM settings WHERE key = "last_login";`,
|
|
||||||
);
|
|
||||||
if (result[0]) {
|
if (result[0]) {
|
||||||
return parseInt(result[0].value);
|
return parseInt(result[0].value);
|
||||||
} else {
|
} else {
|
||||||
@@ -359,7 +336,7 @@ export async function getLastLogin() {
|
|||||||
export async function updateLastLogin(value: number) {
|
export async function updateLastLogin(value: number) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
`UPDATE settings SET value = ${value} WHERE key = "last_login";`,
|
`UPDATE settings SET value = ${value} WHERE key = "last_login";`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +344,7 @@ export async function updateLastLogin(value: number) {
|
|||||||
export async function getBlacklist(account_id: number, kind: number) {
|
export async function getBlacklist(account_id: number, kind: number) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select(
|
return await db.select(
|
||||||
`SELECT * FROM blacklist WHERE account_id = "${account_id}" AND kind = "${kind}";`,
|
`SELECT * FROM blacklist WHERE account_id = "${account_id}" AND kind = "${kind}";`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +352,7 @@ export async function getBlacklist(account_id: number, kind: number) {
|
|||||||
export async function getActiveBlacklist(account_id: number, kind: number) {
|
export async function getActiveBlacklist(account_id: number, kind: number) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.select(
|
return await db.select(
|
||||||
`SELECT content FROM blacklist WHERE account_id = "${account_id}" AND kind = "${kind}" AND status = 1;`,
|
`SELECT content FROM blacklist WHERE account_id = "${account_id}" AND kind = "${kind}" AND status = 1;`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,12 +361,12 @@ export async function addToBlacklist(
|
|||||||
account_id: number,
|
account_id: number,
|
||||||
content: string,
|
content: string,
|
||||||
kind: number,
|
kind: number,
|
||||||
status?: number,
|
status?: number
|
||||||
) {
|
) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"INSERT OR IGNORE INTO blacklist (account_id, content, kind, status) VALUES (?, ?, ?, ?);",
|
'INSERT OR IGNORE INTO blacklist (account_id, content, kind, status) VALUES (?, ?, ?, ?);',
|
||||||
[account_id, content, kind, status || 1],
|
[account_id, content, kind, status || 1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +374,7 @@ export async function addToBlacklist(
|
|||||||
export async function updateItemInBlacklist(content: string, status: number) {
|
export async function updateItemInBlacklist(content: string, status: number) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
`UPDATE blacklist SET status = "${status}" WHERE content = "${content}";`,
|
`UPDATE blacklist SET status = "${status}" WHERE content = "${content}";`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +383,7 @@ export async function getBlocks() {
|
|||||||
const db = await connect();
|
const db = await connect();
|
||||||
const activeAccount = await getActiveAccount();
|
const activeAccount = await getActiveAccount();
|
||||||
const result: any = await db.select(
|
const result: any = await db.select(
|
||||||
`SELECT * FROM blocks WHERE account_id = "${activeAccount.id}" ORDER BY created_at DESC;`,
|
`SELECT * FROM blocks WHERE account_id = "${activeAccount.id}" ORDER BY created_at DESC;`
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -416,8 +393,8 @@ export async function createBlock(kind: number, title: string, content: any) {
|
|||||||
const db = await connect();
|
const db = await connect();
|
||||||
const activeAccount = await getActiveAccount();
|
const activeAccount = await getActiveAccount();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);",
|
'INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);',
|
||||||
[activeAccount.id, kind, title, content],
|
[activeAccount.id, kind, title, content]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,11 +408,11 @@ export async function removeBlock(id: string) {
|
|||||||
export async function removeAll() {
|
export async function removeAll() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
await db.execute(`UPDATE settings SET value = "0" WHERE key = "last_login";`);
|
await db.execute(`UPDATE settings SET value = "0" WHERE key = "last_login";`);
|
||||||
await db.execute("DELETE FROM replies;");
|
await db.execute('DELETE FROM replies;');
|
||||||
await db.execute("DELETE FROM notes;");
|
await db.execute('DELETE FROM notes;');
|
||||||
await db.execute("DELETE FROM blacklist;");
|
await db.execute('DELETE FROM blacklist;');
|
||||||
await db.execute("DELETE FROM blocks;");
|
await db.execute('DELETE FROM blocks;');
|
||||||
await db.execute("DELETE FROM chats;");
|
await db.execute('DELETE FROM chats;');
|
||||||
await db.execute("DELETE FROM accounts;");
|
await db.execute('DELETE FROM accounts;');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main.tsx
19
src/main.tsx
@@ -1,10 +1,13 @@
|
|||||||
import App from "./app";
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { getSetting } from "@libs/storage";
|
import { createRoot } from 'react-dom/client';
|
||||||
import { RelayProvider } from "@shared/relayProvider";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
|
|
||||||
const cacheTime = await getSetting("cache_time");
|
import { getSetting } from '@libs/storage';
|
||||||
|
|
||||||
|
import { RelayProvider } from '@shared/relayProvider';
|
||||||
|
|
||||||
|
import App from './app';
|
||||||
|
|
||||||
|
const cacheTime = await getSetting('cache_time');
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -14,7 +17,7 @@ const queryClient = new QueryClient({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
@@ -22,5 +25,5 @@ root.render(
|
|||||||
<RelayProvider>
|
<RelayProvider>
|
||||||
<App />
|
<App />
|
||||||
</RelayProvider>
|
</RelayProvider>
|
||||||
</QueryClientProvider>,
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { createChat, getLastLogin } from "@libs/storage";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Image } from "@shared/image";
|
import { produce } from 'immer';
|
||||||
import { NetworkStatusIndicator } from "@shared/networkStatusIndicator";
|
import { useContext, useEffect } from 'react';
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { Link } from 'react-router-dom';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { createChat, getLastLogin } from '@libs/storage';
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
|
||||||
import { sendNativeNotification } from "@utils/notification";
|
import { Image } from '@shared/image';
|
||||||
import { produce } from "immer";
|
import { NetworkStatusIndicator } from '@shared/networkStatusIndicator';
|
||||||
import { useContext, useEffect } from "react";
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { sendNativeNotification } from '@utils/notification';
|
||||||
|
|
||||||
const lastLogin = await getLastLogin();
|
const lastLogin = await getLastLogin();
|
||||||
|
|
||||||
@@ -26,23 +30,22 @@ export function ActiveAccount({ data }: { data: any }) {
|
|||||||
data.sender_pubkey,
|
data.sender_pubkey,
|
||||||
data.content,
|
data.content,
|
||||||
data.tags,
|
data.tags,
|
||||||
data.created_at,
|
data.created_at
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSuccess: (data: any) => {
|
onSuccess: (data: any) => {
|
||||||
const prev = queryClient.getQueryData(["chats"]);
|
const prev = queryClient.getQueryData(['chats']);
|
||||||
const next = produce(prev, (draft: any) => {
|
const next = produce(prev, (draft: any) => {
|
||||||
const target = draft.findIndex(
|
const target = draft.findIndex(
|
||||||
(m: { sender_pubkey: string }) => m.sender_pubkey === data,
|
(m: { sender_pubkey: string }) => m.sender_pubkey === data
|
||||||
);
|
);
|
||||||
if (target !== -1) {
|
if (target !== -1) {
|
||||||
draft[target]["new_messages"] =
|
draft[target]['new_messages'] = draft[target]['new_messages'] + 1 || 1;
|
||||||
draft[target]["new_messages"] + 1 || 1;
|
|
||||||
} else {
|
} else {
|
||||||
draft.push({ sender_pubkey: data, new_messages: 1 });
|
draft.push({ sender_pubkey: data, new_messages: 1 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
queryClient.setQueryData(["chats"], next);
|
queryClient.setQueryData(['chats'], next);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,15 +54,15 @@ export function ActiveAccount({ data }: { data: any }) {
|
|||||||
const sub = ndk.subscribe(
|
const sub = ndk.subscribe(
|
||||||
{
|
{
|
||||||
kinds: [4],
|
kinds: [4],
|
||||||
"#p": [data.pubkey],
|
'#p': [data.pubkey],
|
||||||
since: since,
|
since: since,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
closeOnEose: false,
|
closeOnEose: false,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
sub.addListener("event", (event) => {
|
sub.addListener('event', (event) => {
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case 4:
|
case 4:
|
||||||
// update state
|
// update state
|
||||||
@@ -84,15 +87,12 @@ export function ActiveAccount({ data }: { data: any }) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (status === "loading") {
|
if (status === 'loading') {
|
||||||
return <div className="w-9 h-9 rounded-md bg-zinc-800 animate-pulse" />;
|
return <div className="h-9 w-9 animate-pulse rounded-md bg-zinc-800" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link to={`/app/user/${data.pubkey}`} className="relative inline-block h-9 w-9">
|
||||||
to={`/app/user/${data.pubkey}`}
|
|
||||||
className="relative inline-block h-9 w-9"
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
src={user.image}
|
src={user.image}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from '@shared/image';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
|
||||||
export function InactiveAccount({ data }: { data: any }) {
|
export function InactiveAccount({ data }: { data: any }) {
|
||||||
const { user } = useProfile(data.npub);
|
const { user } = useProfile(data.npub);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function AppHeader({ reverse }: { reverse?: boolean }) {
|
export function AppHeader({ reverse }: { reverse?: boolean }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -15,8 +16,8 @@ export function AppHeader({ reverse }: { reverse?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className={`shrink-0 flex h-11 w-full px-3 border-b border-zinc-900 items-center ${
|
className={`flex h-11 w-full shrink-0 items-center border-b border-zinc-900 px-3 ${
|
||||||
reverse ? "justify-start" : "justify-end"
|
reverse ? 'justify-start' : 'justify-end'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Navigation } from "@shared/navigation";
|
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
||||||
import { Outlet, ScrollRestoration } from "react-router-dom";
|
|
||||||
|
import { Navigation } from '@shared/navigation';
|
||||||
|
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-screen h-screen">
|
<div className="flex h-screen w-screen">
|
||||||
<div className="relative flex flex-row shrink-0">
|
<div className="relative flex shrink-0 flex-row">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full">
|
<div className="h-full w-full">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons";
|
import { platform } from '@tauri-apps/api/os';
|
||||||
import { platform } from "@tauri-apps/api/os";
|
import { Outlet, useNavigate } from 'react-router-dom';
|
||||||
import { Outlet, useNavigate } from "react-router-dom";
|
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon } from '@shared/icons';
|
||||||
|
|
||||||
const platformName = await platform();
|
const platformName = await platform();
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ export function AuthLayout() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex h-full items-center gap-2 ${
|
className={`flex h-full items-center gap-2 ${
|
||||||
platformName === "darwin" ? "pl-[68px]" : ""
|
platformName === 'darwin' ? 'pl-[68px]' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { LoaderIcon, PlusIcon } from "@shared/icons";
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
import { open } from "@tauri-apps/api/dialog";
|
import { Body, fetch } from '@tauri-apps/api/http';
|
||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
import { useState } from 'react';
|
||||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
|
||||||
import { useState } from "react";
|
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { createBlobFromFile } from '@utils/createBlobFromFile';
|
||||||
|
|
||||||
export function AvatarUploader({ setPicture }: { setPicture: any }) {
|
export function AvatarUploader({ setPicture }: { setPicture: any }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -12,8 +14,8 @@ export function AvatarUploader({ setPicture }: { setPicture: any }) {
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: "Image",
|
name: 'Image',
|
||||||
extensions: ["png", "jpeg", "jpg", "gif"],
|
extensions: ['png', 'jpeg', 'jpg', 'gif'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -24,24 +26,24 @@ export function AvatarUploader({ setPicture }: { setPicture: any }) {
|
|||||||
} else {
|
} else {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const filename = selected.split("/").pop();
|
const filename = selected.split('/').pop();
|
||||||
const file = await createBlobFromFile(selected);
|
const file = await createBlobFromFile(selected);
|
||||||
const buf = await file.arrayBuffer();
|
const buf = await file.arrayBuffer();
|
||||||
|
|
||||||
const res: { data: { file: { id: string } } } = await fetch(
|
const res: { data: { file: { id: string } } } = await fetch(
|
||||||
"https://void.cat/upload?cli=false",
|
'https://void.cat/upload?cli=false',
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
headers: {
|
headers: {
|
||||||
accept: "*/*",
|
accept: '*/*',
|
||||||
"Content-Type": "application/octet-stream",
|
'Content-Type': 'application/octet-stream',
|
||||||
"V-Filename": filename,
|
'V-Filename': filename,
|
||||||
"V-Description": "Upload from https://lume.nu",
|
'V-Description': 'Upload from https://lume.nu',
|
||||||
"V-Strip-Metadata": "true",
|
'V-Strip-Metadata': 'true',
|
||||||
},
|
},
|
||||||
body: Body.bytes(buf),
|
body: Body.bytes(buf),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const image = `https://void.cat/d/${res.data.file.id}.jpg`;
|
const image = `https://void.cat/d/${res.data.file.id}.jpg`;
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ export function AvatarUploader({ setPicture }: { setPicture: any }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openFileDialog()}
|
onClick={() => openFileDialog()}
|
||||||
className="w-full h-full inline-flex items-center justify-center bg-zinc-900/40"
|
className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-6 w-6 animate-spin text-zinc-100" />
|
<LoaderIcon className="h-6 w-6 animate-spin text-zinc-100" />
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { LoaderIcon, PlusIcon } from "@shared/icons";
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
import { open } from "@tauri-apps/api/dialog";
|
import { Body, fetch } from '@tauri-apps/api/http';
|
||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
import { useState } from 'react';
|
||||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
|
||||||
import { useState } from "react";
|
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { createBlobFromFile } from '@utils/createBlobFromFile';
|
||||||
|
|
||||||
export function BannerUploader({ setBanner }: { setBanner: any }) {
|
export function BannerUploader({ setBanner }: { setBanner: any }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -12,8 +14,8 @@ export function BannerUploader({ setBanner }: { setBanner: any }) {
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: "Image",
|
name: 'Image',
|
||||||
extensions: ["png", "jpeg", "jpg", "gif"],
|
extensions: ['png', 'jpeg', 'jpg', 'gif'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -24,24 +26,24 @@ export function BannerUploader({ setBanner }: { setBanner: any }) {
|
|||||||
} else {
|
} else {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const filename = selected.split("/").pop();
|
const filename = selected.split('/').pop();
|
||||||
const file = await createBlobFromFile(selected);
|
const file = await createBlobFromFile(selected);
|
||||||
const buf = await file.arrayBuffer();
|
const buf = await file.arrayBuffer();
|
||||||
|
|
||||||
const res: { data: { file: { id: string } } } = await fetch(
|
const res: { data: { file: { id: string } } } = await fetch(
|
||||||
"https://void.cat/upload?cli=false",
|
'https://void.cat/upload?cli=false',
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
headers: {
|
headers: {
|
||||||
accept: "*/*",
|
accept: '*/*',
|
||||||
"Content-Type": "application/octet-stream",
|
'Content-Type': 'application/octet-stream',
|
||||||
"V-Filename": filename,
|
'V-Filename': filename,
|
||||||
"V-Description": "Upload from https://lume.nu",
|
'V-Description': 'Upload from https://lume.nu',
|
||||||
"V-Strip-Metadata": "true",
|
'V-Strip-Metadata': 'true',
|
||||||
},
|
},
|
||||||
body: Body.bytes(buf),
|
body: Body.bytes(buf),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const image = `https://void.cat/d/${res.data.file.id}.jpg`;
|
const image = `https://void.cat/d/${res.data.file.id}.jpg`;
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ export function BannerUploader({ setBanner }: { setBanner: any }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openFileDialog()}
|
onClick={() => openFileDialog()}
|
||||||
className="w-full h-full inline-flex items-center justify-center bg-zinc-900/40"
|
className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-8 w-8 animate-spin text-zinc-100" />
|
<LoaderIcon className="h-8 w-8 animate-spin text-zinc-100" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from 'react';
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
preset,
|
preset,
|
||||||
@@ -7,24 +7,24 @@ export function Button({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
onClick = undefined,
|
onClick = undefined,
|
||||||
}: {
|
}: {
|
||||||
preset: "small" | "publish" | "large";
|
preset: 'small' | 'publish' | 'large';
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
let preClass: string;
|
let preClass: string;
|
||||||
switch (preset) {
|
switch (preset) {
|
||||||
case "small":
|
case 'small':
|
||||||
preClass =
|
preClass =
|
||||||
"w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600";
|
'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600';
|
||||||
break;
|
break;
|
||||||
case "publish":
|
case 'publish':
|
||||||
preClass =
|
preClass =
|
||||||
"w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600";
|
'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600';
|
||||||
break;
|
break;
|
||||||
case "large":
|
case 'large':
|
||||||
preClass =
|
preClass =
|
||||||
"h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600";
|
'h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -36,8 +36,8 @@ export function Button({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"inline-flex items-center justify-center gap-1 transform active:translate-y-1 disabled:pointer-events-none disabled:opacity-50 focus:outline-none",
|
'inline-flex transform items-center justify-center gap-1 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
|
||||||
preClass,
|
preClass
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,42 +1,44 @@
|
|||||||
import { PlusCircleIcon } from "@shared/icons";
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
import { open } from "@tauri-apps/api/dialog";
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { Body, fetch } from '@tauri-apps/api/http';
|
||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
import { Transforms } from 'slate';
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useSlateStatic } from 'slate-react';
|
||||||
import { Transforms } from "slate";
|
|
||||||
import { useSlateStatic } from "slate-react";
|
import { PlusCircleIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { createBlobFromFile } from '@utils/createBlobFromFile';
|
||||||
|
|
||||||
export function ImageUploader() {
|
export function ImageUploader() {
|
||||||
const editor = useSlateStatic();
|
const editor = useSlateStatic();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const insertImage = (editor, url) => {
|
const insertImage = (editor, url) => {
|
||||||
const image = { type: "image", url, children: [{ text: url }] };
|
const image = { type: 'image', url, children: [{ text: url }] };
|
||||||
Transforms.insertNodes(editor, image);
|
Transforms.insertNodes(editor, image);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadToVoidCat = useCallback(
|
const uploadToVoidCat = useCallback(
|
||||||
async (filepath) => {
|
async (filepath) => {
|
||||||
const filename = filepath.split("/").pop();
|
const filename = filepath.split('/').pop();
|
||||||
const file = await createBlobFromFile(filepath);
|
const file = await createBlobFromFile(filepath);
|
||||||
const buf = await file.arrayBuffer();
|
const buf = await file.arrayBuffer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: { data: { file: { id: string } } } = await fetch(
|
const res: { data: { file: { id: string } } } = await fetch(
|
||||||
"https://void.cat/upload?cli=false",
|
'https://void.cat/upload?cli=false',
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
headers: {
|
headers: {
|
||||||
accept: "*/*",
|
accept: '*/*',
|
||||||
"Content-Type": "application/octet-stream",
|
'Content-Type': 'application/octet-stream',
|
||||||
"V-Filename": filename,
|
'V-Filename': filename,
|
||||||
"V-Description": "Uploaded from https://lume.nu",
|
'V-Description': 'Uploaded from https://lume.nu',
|
||||||
"V-Strip-Metadata": "true",
|
'V-Strip-Metadata': 'true',
|
||||||
},
|
},
|
||||||
body: Body.bytes(buf),
|
body: Body.bytes(buf),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const image = `https://void.cat/d/${res.data.file.id}.webp`;
|
const image = `https://void.cat/d/${res.data.file.id}.webp`;
|
||||||
// update parent state
|
// update parent state
|
||||||
@@ -49,13 +51,13 @@ export function ImageUploader() {
|
|||||||
// handle error
|
// handle error
|
||||||
if (error instanceof SyntaxError) {
|
if (error instanceof SyntaxError) {
|
||||||
// Unexpected token < in JSON
|
// Unexpected token < in JSON
|
||||||
console.log("There was a SyntaxError", error);
|
console.log('There was a SyntaxError', error);
|
||||||
} else {
|
} else {
|
||||||
console.log("There was an error", error);
|
console.log('There was an error', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[editor],
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
const openFileDialog = async () => {
|
const openFileDialog = async () => {
|
||||||
@@ -63,8 +65,8 @@ export function ImageUploader() {
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: "Image",
|
name: 'Image',
|
||||||
extensions: ["png", "jpeg", "jpg", "gif"],
|
extensions: ['png', 'jpeg', 'jpg', 'gif'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -81,7 +83,7 @@ export function ImageUploader() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function initFileDrop() {
|
async function initFileDrop() {
|
||||||
const unlisten = await listen("tauri://file-drop", (event) => {
|
const unlisten = await listen('tauri://file-drop', (event) => {
|
||||||
// set loading state
|
// set loading state
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// upload file
|
// upload file
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { Button } from "@shared/button";
|
import { Fragment } from 'react';
|
||||||
import { Post } from "@shared/composer/types/post";
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { User } from "@shared/composer/user";
|
|
||||||
|
import { Button } from '@shared/button';
|
||||||
|
import { Post } from '@shared/composer/types/post';
|
||||||
|
import { User } from '@shared/composer/user';
|
||||||
import {
|
import {
|
||||||
CancelIcon,
|
CancelIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ComposeIcon,
|
ComposeIcon,
|
||||||
} from "@shared/icons";
|
} from '@shared/icons';
|
||||||
import { useComposer } from "@stores/composer";
|
|
||||||
import { COMPOSE_SHORTCUT } from "@stores/shortcuts";
|
import { useComposer } from '@stores/composer';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { COMPOSE_SHORTCUT } from '@stores/shortcuts';
|
||||||
import { Fragment } from "react";
|
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
|
|
||||||
export function Composer() {
|
export function Composer() {
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
|
|
||||||
const [toggle, open] = useComposer((state) => [
|
const [toggle, open] = useComposer((state) => [state.toggleModal, state.open]);
|
||||||
state.toggleModal,
|
|
||||||
state.open,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
toggle(false);
|
toggle(false);
|
||||||
@@ -76,13 +76,11 @@ export function Composer() {
|
|||||||
<div
|
<div
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
onKeyDown={closeModal}
|
onKeyDown={closeModal}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
|
||||||
>
|
>
|
||||||
<CancelIcon
|
<CancelIcon width={16} height={16} className="text-zinc-500" />
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className="text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{account && <Post />}
|
{account && <Post />}
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import { usePublish } from "@libs/ndk";
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { Button } from "@shared/button";
|
import { Node, Transforms, createEditor } from 'slate';
|
||||||
import { ImageUploader } from "@shared/composer/imageUploader";
|
import { withHistory } from 'slate-history';
|
||||||
import { TrashIcon } from "@shared/icons";
|
import { Editable, ReactEditor, Slate, useSlateStatic, withReact } from 'slate-react';
|
||||||
import { MentionNote } from "@shared/notes/mentions/note";
|
|
||||||
import { useComposer } from "@stores/composer";
|
import { usePublish } from '@libs/ndk';
|
||||||
import { FULL_RELAYS } from "@stores/constants";
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { Button } from '@shared/button';
|
||||||
import { Node, Transforms, createEditor } from "slate";
|
import { ImageUploader } from '@shared/composer/imageUploader';
|
||||||
import { withHistory } from "slate-history";
|
import { TrashIcon } from '@shared/icons';
|
||||||
import {
|
import { MentionNote } from '@shared/notes/mentions/note';
|
||||||
Editable,
|
|
||||||
ReactEditor,
|
import { useComposer } from '@stores/composer';
|
||||||
Slate,
|
import { FULL_RELAYS } from '@stores/constants';
|
||||||
useSlateStatic,
|
|
||||||
withReact,
|
|
||||||
} from "slate-react";
|
|
||||||
|
|
||||||
const withImages = (editor) => {
|
const withImages = (editor) => {
|
||||||
const { isVoid } = editor;
|
const { isVoid } = editor;
|
||||||
|
|
||||||
editor.isVoid = (element) => {
|
editor.isVoid = (element) => {
|
||||||
return element.type === "image" ? true : isVoid(element);
|
return element.type === 'image' ? true : isVoid(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
@@ -50,7 +47,7 @@ const ImagePreview = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||||
className="absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center gap-0.5 rounded bg-zinc-800 text-base font-medium text-zinc-400 shadow-mini-button hover:bg-zinc-700"
|
className="shadow-mini-button absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center gap-0.5 rounded bg-zinc-800 text-base font-medium text-zinc-400 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
<TrashIcon width={14} height={14} className="text-zinc-100" />
|
<TrashIcon width={14} height={14} className="text-zinc-100" />
|
||||||
</button>
|
</button>
|
||||||
@@ -61,10 +58,7 @@ const ImagePreview = ({
|
|||||||
|
|
||||||
export function Post() {
|
export function Post() {
|
||||||
const publish = usePublish();
|
const publish = usePublish();
|
||||||
const editor = useMemo(
|
const editor = useMemo(() => withReact(withImages(withHistory(createEditor()))), []);
|
||||||
() => withReact(withImages(withHistory(createEditor()))),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [repost, reply, toggle] = useComposer((state) => [
|
const [repost, reply, toggle] = useComposer((state) => [
|
||||||
state.repost,
|
state.repost,
|
||||||
@@ -75,14 +69,14 @@ export function Post() {
|
|||||||
{
|
{
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: "",
|
text: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const serialize = useCallback((nodes: Node[]) => {
|
const serialize = useCallback((nodes: Node[]) => {
|
||||||
return nodes.map((n) => Node.string(n)).join("\n");
|
return nodes.map((n) => Node.string(n)).join('\n');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getRef = () => {
|
const getRef = () => {
|
||||||
@@ -104,21 +98,21 @@ export function Post() {
|
|||||||
if (repost.id && repost.pubkey) {
|
if (repost.id && repost.pubkey) {
|
||||||
kind = 6;
|
kind = 6;
|
||||||
tags = [
|
tags = [
|
||||||
["e", repost.id, FULL_RELAYS[0], "root"],
|
['e', repost.id, FULL_RELAYS[0], 'root'],
|
||||||
["p", repost.pubkey],
|
['p', repost.pubkey],
|
||||||
];
|
];
|
||||||
} else if (reply.id && reply.pubkey) {
|
} else if (reply.id && reply.pubkey) {
|
||||||
kind = 1;
|
kind = 1;
|
||||||
if (reply.root && reply.root !== reply.id) {
|
if (reply.root && reply.root !== reply.id) {
|
||||||
tags = [
|
tags = [
|
||||||
["e", reply.id, FULL_RELAYS[0], "root"],
|
['e', reply.id, FULL_RELAYS[0], 'root'],
|
||||||
["e", reply.root, FULL_RELAYS[0], "reply"],
|
['e', reply.root, FULL_RELAYS[0], 'reply'],
|
||||||
["p", reply.pubkey],
|
['p', reply.pubkey],
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
tags = [
|
tags = [
|
||||||
["e", reply.id, FULL_RELAYS[0], "root"],
|
['e', reply.id, FULL_RELAYS[0], 'root'],
|
||||||
["p", reply.pubkey],
|
['p', reply.pubkey],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -138,10 +132,11 @@ export function Post() {
|
|||||||
|
|
||||||
const renderElement = useCallback((props: any) => {
|
const renderElement = useCallback((props: any) => {
|
||||||
switch (props.element.type) {
|
switch (props.element.type) {
|
||||||
case "image":
|
case 'image':
|
||||||
if (props.element.url) {
|
if (props.element.url) {
|
||||||
return <ImagePreview {...props} />;
|
return <ImagePreview {...props} />;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return <p {...props.attributes}>{props.children}</p>;
|
return <p {...props.attributes}>{props.children}</p>;
|
||||||
}
|
}
|
||||||
@@ -156,12 +151,9 @@ export function Post() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Editable
|
<Editable
|
||||||
autoFocus
|
placeholder={refID ? 'Share your thoughts on it' : "What's on your mind?"}
|
||||||
placeholder={
|
|
||||||
refID ? "Share your thoughts on it" : "What's on your mind?"
|
|
||||||
}
|
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className={`${refID ? "!min-h-42" : "!min-h-[86px]"} markdown`}
|
className={`${refID ? '!min-h-42' : '!min-h-[86px]'} markdown`}
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
/>
|
/>
|
||||||
{refID && <MentionNote id={refID} />}
|
{refID && <MentionNote id={refID} />}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from '@shared/image';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
|
||||||
export function User({ pubkey }: { pubkey: string }) {
|
export function User({ pubkey }: { pubkey: string }) {
|
||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { usePublish } from "@libs/ndk";
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { AvatarUploader } from "@shared/avatarUploader";
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
import { BannerUploader } from "@shared/bannerUploader";
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
import {
|
import { useForm } from 'react-hook-form';
|
||||||
CancelIcon,
|
|
||||||
CheckCircleIcon,
|
import { usePublish } from '@libs/ndk';
|
||||||
LoaderIcon,
|
|
||||||
UnverifiedIcon,
|
import { AvatarUploader } from '@shared/avatarUploader';
|
||||||
} from "@shared/icons";
|
import { BannerUploader } from '@shared/bannerUploader';
|
||||||
import { Image } from "@shared/image";
|
import { CancelIcon, CheckCircleIcon, LoaderIcon, UnverifiedIcon } from '@shared/icons';
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { Image } from '@shared/image';
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { fetch } from "@tauri-apps/api/http";
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
export function EditProfileModal() {
|
export function EditProfileModal() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -24,8 +23,8 @@ export function EditProfileModal() {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
||||||
const [banner, setBanner] = useState("");
|
const [banner, setBanner] = useState('');
|
||||||
const [nip05, setNIP05] = useState({ verified: false, text: "" });
|
const [nip05, setNIP05] = useState({ verified: false, text: '' });
|
||||||
|
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
const {
|
const {
|
||||||
@@ -36,7 +35,7 @@ export function EditProfileModal() {
|
|||||||
formState: { isValid, errors },
|
formState: { isValid, errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: async () => {
|
defaultValues: async () => {
|
||||||
const res: any = queryClient.getQueryData(["user", account.pubkey]);
|
const res: any = queryClient.getQueryData(['user', account.pubkey]);
|
||||||
if (res.image) {
|
if (res.image) {
|
||||||
setPicture(res.image);
|
setPicture(res.image);
|
||||||
}
|
}
|
||||||
@@ -60,16 +59,16 @@ export function EditProfileModal() {
|
|||||||
|
|
||||||
const verifyNIP05 = async (data: string) => {
|
const verifyNIP05 = async (data: string) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const url = data.split("@");
|
const url = data.split('@');
|
||||||
const username = url[0];
|
const username = url[0];
|
||||||
const service = url[1];
|
const service = url[1];
|
||||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`;
|
const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`;
|
||||||
|
|
||||||
const res: any = await fetch(verifyURL, {
|
const res: any = await fetch(verifyURL, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,8 +106,8 @@ export function EditProfileModal() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setNIP05((prev) => ({ ...prev, verified: false }));
|
setNIP05((prev) => ({ ...prev, verified: false }));
|
||||||
setError("nip05", {
|
setError('nip05', {
|
||||||
type: "manual",
|
type: 'manual',
|
||||||
message: "Can't verify your Lume ID / NIP-05, please check again",
|
message: "Can't verify your Lume ID / NIP-05, please check again",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -123,7 +122,7 @@ export function EditProfileModal() {
|
|||||||
if (event.id) {
|
if (event.id) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// invalid cache
|
// invalid cache
|
||||||
queryClient.invalidateQueries(["user", account.pubkey]);
|
queryClient.invalidateQueries(['user', account.pubkey]);
|
||||||
// reset form
|
// reset form
|
||||||
reset();
|
reset();
|
||||||
// reset state
|
// reset state
|
||||||
@@ -148,7 +147,7 @@ export function EditProfileModal() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
Edit profile
|
Edit profile
|
||||||
</button>
|
</button>
|
||||||
@@ -189,45 +188,45 @@ export function EditProfileModal() {
|
|||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<CancelIcon className="w-5 h-5 text-zinc-300" />
|
<CancelIcon className="h-5 w-5 text-zinc-300" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0">
|
||||||
<input
|
<input
|
||||||
type={"hidden"}
|
type={'hidden'}
|
||||||
{...register("picture")}
|
{...register('picture')}
|
||||||
value={picture}
|
value={picture}
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type={"hidden"}
|
type={'hidden'}
|
||||||
{...register("banner")}
|
{...register('banner')}
|
||||||
value={banner}
|
value={banner}
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative w-full h-44 bg-zinc-800">
|
<div className="relative h-44 w-full bg-zinc-800">
|
||||||
<Image
|
<Image
|
||||||
src={banner}
|
src={banner}
|
||||||
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
|
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
|
||||||
alt="user's banner"
|
alt="user's banner"
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full">
|
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
<BannerUploader setBanner={setBanner} />
|
<BannerUploader setBanner={setBanner} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 mb-5">
|
<div className="mb-5 px-4">
|
||||||
<div className="z-10 relative h-14 w-14 -mt-7">
|
<div className="relative z-10 -mt-7 h-14 w-14">
|
||||||
<Image
|
<Image
|
||||||
src={picture}
|
src={picture}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt="user's avatar"
|
alt="user's avatar"
|
||||||
className="h-14 w-14 object-cover ring-2 ring-zinc-900 rounded-lg"
|
className="h-14 w-14 rounded-lg object-cover ring-2 ring-zinc-900"
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full">
|
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
<AvatarUploader setPicture={setPicture} />
|
<AvatarUploader setPicture={setPicture} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,41 +234,47 @@ export function EditProfileModal() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Name
|
Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={"text"}
|
type={'text'}
|
||||||
{...register("name", {
|
{...register('name', {
|
||||||
required: true,
|
required: true,
|
||||||
minLength: 4,
|
minLength: 4,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="nip05"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Lume ID / NIP-05
|
Lume ID / NIP-05
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
{...register("nip05", {
|
{...register('nip05', {
|
||||||
required: true,
|
required: true,
|
||||||
minLength: 4,
|
minLength: 4,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-1/2 right-2 transform -translate-y-1/2">
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
|
||||||
{nip05.verified ? (
|
{nip05.verified ? (
|
||||||
<span className="inline-flex items-center gap-1 rounded h-6 px-2 bg-green-500 text-sm font-medium">
|
<span className="inline-flex h-6 items-center gap-1 rounded bg-green-500 px-2 text-sm font-medium">
|
||||||
<CheckCircleIcon className="w-4 h-4 text-white" />
|
<CheckCircleIcon className="h-4 w-4 text-white" />
|
||||||
Verified
|
Verified
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1 rounded h-6 px-2 bg-red-500 text-sm font-medium">
|
<span className="inline-flex h-6 items-center gap-1 rounded bg-red-500 px-2 text-sm font-medium">
|
||||||
<UnverifiedIcon className="w-4 h-4 text-white" />
|
<UnverifiedIcon className="h-4 w-4 text-white" />
|
||||||
Unverified
|
Unverified
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -282,36 +287,42 @@ export function EditProfileModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="about"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Bio
|
Bio
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
{...register("about")}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative resize-none h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
<label
|
||||||
|
htmlFor="website"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
Website
|
Website
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={"text"}
|
type={'text'}
|
||||||
{...register("website", { required: false })}
|
{...register('website', { required: false })}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="inline-flex items-center justify-center gap-1 transform active:translate-y-1 disabled:pointer-events-none disabled:opacity-50 focus:outline-none h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
) : (
|
) : (
|
||||||
"Update"
|
'Update'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function ArrowLeftIcon(
|
export function ArrowLeftIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M10 18.25L3.75 12M3.75 12L10 5.75M3.75 12H20.25"
|
d="M10 18.25L3.75 12M3.75 12L10 5.75M3.75 12H20.25"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function ArrowRightIcon(
|
export function ArrowRightIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M14 5.75L20.25 12M20.25 12L14 18.25M20.25 12H3.75"
|
d="M14 5.75L20.25 12M20.25 12L14 18.25M20.25 12H3.75"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function ArrowRightCircleIcon(
|
export function ArrowRightCircleIcon(
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function BellIcon(
|
export function BellIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width={24}
|
width={24}
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function CancelIcon(
|
export function CancelIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M4.75 4.75L19.25 19.25M19.25 4.75L4.75 19.25"
|
d="M4.75 4.75L19.25 19.25M19.25 4.75L4.75 19.25"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function CheckCircleIcon(
|
export function CheckCircleIcon(
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function ChevronDownIcon(
|
export function ChevronDownIcon(
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function ChevronRightIcon(
|
export function ChevronRightIcon(
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function CommandIcon(
|
export function CommandIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function ComposeIcon(
|
export function ComposeIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width={24}
|
width={24}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function CopyIcon(
|
export function CopyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width={24}
|
width={24}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function EditIcon(
|
export function EditIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width={24}
|
width={24}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { SVGProps } from "react";
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
export function EmptyIcon(
|
export function EmptyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -52,10 +50,7 @@ export function EmptyIcon(
|
|||||||
>
|
>
|
||||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
<feGaussianBlur
|
<feGaussianBlur result="effect1_foregroundBlur_110_63" stdDeviation="5.5" />
|
||||||
result="effect1_foregroundBlur_110_63"
|
|
||||||
stdDeviation="5.5"
|
|
||||||
/>
|
|
||||||
</filter>
|
</filter>
|
||||||
<clipPath id="clip0_110_63">
|
<clipPath id="clip0_110_63">
|
||||||
<path fill="#fff" d="M0 0H120V120H0z" />
|
<path fill="#fff" d="M0 0H120V120H0z" />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user