mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
wip: network
This commit is contained in:
parent
373a0f0608
commit
71338b3b07
@ -62,6 +62,7 @@
|
|||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"light-bolt11-decoder": "^3.0.0",
|
"light-bolt11-decoder": "^3.0.0",
|
||||||
|
"lru-cache": "^10.0.0",
|
||||||
"million": "2.5.4-beta.2",
|
"million": "2.5.4-beta.2",
|
||||||
"nostr-fetch": "^0.12.2",
|
"nostr-fetch": "^0.12.2",
|
||||||
"nostr-tools": "^1.14.0",
|
"nostr-tools": "^1.14.0",
|
||||||
|
@ -136,6 +136,9 @@ dependencies:
|
|||||||
light-bolt11-decoder:
|
light-bolt11-decoder:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
lru-cache:
|
||||||
|
specifier: ^10.0.0
|
||||||
|
version: 10.0.0
|
||||||
million:
|
million:
|
||||||
specifier: 2.5.4-beta.2
|
specifier: 2.5.4-beta.2
|
||||||
version: 2.5.4-beta.2
|
version: 2.5.4-beta.2
|
||||||
|
BIN
public/lume.png
Normal file
BIN
public/lume.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
@ -4,11 +4,11 @@
|
|||||||
)]
|
)]
|
||||||
|
|
||||||
// use rand::distributions::{Alphanumeric, DistString};
|
// use rand::distributions::{Alphanumeric, DistString};
|
||||||
use tauri::{Manager};
|
use tauri::Manager;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||||
use window_shadows::set_shadow;
|
use window_shadows::set_shadow;
|
||||||
use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial};
|
use window_vibrancy::{apply_mica, apply_vibrancy, NSVisualEffectMaterial};
|
||||||
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
struct Payload {
|
struct Payload {
|
||||||
@ -16,6 +16,16 @@ struct Payload {
|
|||||||
cwd: String,
|
cwd: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn close_splashscreen(window: tauri::Window) {
|
||||||
|
// Close splashscreen
|
||||||
|
if let Some(splashscreen) = window.get_window("splashscreen") {
|
||||||
|
splashscreen.close().unwrap();
|
||||||
|
}
|
||||||
|
// Show main window
|
||||||
|
window.get_window("main").unwrap().show().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(
|
.plugin(
|
||||||
@ -147,7 +157,7 @@ fn main() {
|
|||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let window = app.get_window("main").unwrap();
|
let window = app.get_window("main").unwrap();
|
||||||
|
|
||||||
// native shadow
|
// native shadow
|
||||||
set_shadow(&window, true).expect("Unsupported platform!");
|
set_shadow(&window, true).expect("Unsupported platform!");
|
||||||
|
|
||||||
@ -156,11 +166,12 @@ fn main() {
|
|||||||
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
apply_blur(&window, Some((18, 18, 18, 125)))
|
apply_mica(&window, None, None)
|
||||||
.expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
.expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![close_splashscreen])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -94,20 +94,32 @@
|
|||||||
},
|
},
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"fullscreen": false,
|
"width": 1080,
|
||||||
"hiddenTitle": true,
|
"height": 800,
|
||||||
"minHeight": 400,
|
"minWidth": 1080,
|
||||||
"minWidth": 500,
|
"minHeight": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"theme": "Dark",
|
"theme": "Dark",
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
"titleBarStyle": "Overlay",
|
"titleBarStyle": "Overlay",
|
||||||
"transparent": true,
|
"transparent": true,
|
||||||
|
"center": true,
|
||||||
|
"fullscreen": false,
|
||||||
|
"hiddenTitle": true,
|
||||||
|
"visible": false
|
||||||
|
},
|
||||||
|
{
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 500,
|
"height": 500,
|
||||||
"center": true
|
"decorations": true,
|
||||||
|
"hiddenTitle": true,
|
||||||
|
"center": true,
|
||||||
|
"resizable": false,
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"label": "splashscreen",
|
||||||
|
"url": "splashscreen"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"macOSPrivateApi": true
|
"macOSPrivateApi": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
75
src/app.tsx
75
src/app.tsx
@ -1,4 +1,4 @@
|
|||||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
|
import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { AuthCreateScreen } from '@app/auth/create';
|
import { AuthCreateScreen } from '@app/auth/create';
|
||||||
import { CreateStep1Screen } from '@app/auth/create/step-1';
|
import { CreateStep1Screen } from '@app/auth/create/step-1';
|
||||||
@ -15,33 +15,62 @@ import { OnboardingScreen } from '@app/auth/onboarding';
|
|||||||
import { ResetScreen } from '@app/auth/reset';
|
import { ResetScreen } from '@app/auth/reset';
|
||||||
import { UnlockScreen } from '@app/auth/unlock';
|
import { UnlockScreen } from '@app/auth/unlock';
|
||||||
import { WelcomeScreen } from '@app/auth/welcome';
|
import { WelcomeScreen } from '@app/auth/welcome';
|
||||||
import { ChannelScreen } from '@app/channel';
|
|
||||||
import { ChatScreen } from '@app/chats';
|
import { ChatScreen } from '@app/chats';
|
||||||
import { ErrorScreen } from '@app/error';
|
import { ErrorScreen } from '@app/error';
|
||||||
import { EventScreen } from '@app/events';
|
import { EventScreen } from '@app/events';
|
||||||
import { Root } from '@app/root';
|
|
||||||
import { AccountSettingsScreen } from '@app/settings/account';
|
import { AccountSettingsScreen } from '@app/settings/account';
|
||||||
import { GeneralSettingsScreen } from '@app/settings/general';
|
import { GeneralSettingsScreen } from '@app/settings/general';
|
||||||
import { ShortcutsSettingsScreen } from '@app/settings/shortcuts';
|
import { ShortcutsSettingsScreen } from '@app/settings/shortcuts';
|
||||||
import { SpaceScreen } from '@app/space';
|
import { SpaceScreen } from '@app/space';
|
||||||
|
import { SplashScreen } from '@app/splash';
|
||||||
import { TrendingScreen } from '@app/trending';
|
import { TrendingScreen } from '@app/trending';
|
||||||
import { UserScreen } from '@app/users';
|
import { UserScreen } from '@app/users';
|
||||||
|
|
||||||
|
import { getActiveAccount } from '@libs/storage';
|
||||||
|
|
||||||
import { AppLayout } from '@shared/appLayout';
|
import { AppLayout } from '@shared/appLayout';
|
||||||
import { AuthLayout } from '@shared/authLayout';
|
import { AuthLayout } from '@shared/authLayout';
|
||||||
import { Protected } from '@shared/protected';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
import { SettingsLayout } from '@shared/settingsLayout';
|
import { SettingsLayout } from '@shared/settingsLayout';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
const appLoader = async () => {
|
||||||
|
const account = await getActiveAccount();
|
||||||
|
const privkey = sessionStorage.getItem('stronghold');
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return redirect('/auth/welcome');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account && account.privkey.length > 35) {
|
||||||
|
return redirect('/auth/migrate');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account && !privkey) {
|
||||||
|
return redirect('/auth/unlock');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: (
|
element: <AppLayout />,
|
||||||
<Protected>
|
errorElement: <ErrorScreen />,
|
||||||
<Root />
|
loader: appLoader,
|
||||||
</Protected>
|
children: [
|
||||||
),
|
{ path: '', element: <SpaceScreen /> },
|
||||||
|
{ path: 'trending', element: <TrendingScreen /> },
|
||||||
|
{ path: 'events/:id', element: <EventScreen /> },
|
||||||
|
{ path: 'users/:pubkey', element: <UserScreen /> },
|
||||||
|
{ path: 'chats/:pubkey', element: <ChatScreen /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/splashscreen',
|
||||||
|
element: <SplashScreen />,
|
||||||
errorElement: <ErrorScreen />,
|
errorElement: <ErrorScreen />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -75,29 +104,9 @@ const router = createBrowserRouter([
|
|||||||
{ path: 'reset', element: <ResetScreen /> },
|
{ path: 'reset', element: <ResetScreen /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/app',
|
|
||||||
element: (
|
|
||||||
<Protected>
|
|
||||||
<AppLayout />
|
|
||||||
</Protected>
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{ path: 'space', element: <SpaceScreen /> },
|
|
||||||
{ path: 'trending', element: <TrendingScreen /> },
|
|
||||||
{ path: 'events/:id', element: <EventScreen /> },
|
|
||||||
{ path: 'users/:pubkey', element: <UserScreen /> },
|
|
||||||
{ path: 'chats/:pubkey', element: <ChatScreen /> },
|
|
||||||
{ path: 'channel/:id', element: <ChannelScreen /> },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
element: (
|
element: <SettingsLayout />,
|
||||||
<Protected>
|
|
||||||
<SettingsLayout />
|
|
||||||
</Protected>
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
{ path: 'general', element: <GeneralSettingsScreen /> },
|
{ path: 'general', element: <GeneralSettingsScreen /> },
|
||||||
{ path: 'shortcuts', element: <ShortcutsSettingsScreen /> },
|
{ path: 'shortcuts', element: <ShortcutsSettingsScreen /> },
|
||||||
@ -110,7 +119,11 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
router={router}
|
router={router}
|
||||||
fallbackElement={<p>Loading..</p>}
|
fallbackElement={
|
||||||
|
<div className="flex h-full w-full items-center justify-center bg-black/90">
|
||||||
|
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
future={{ v7_startTransition: true }}
|
future={{ v7_startTransition: true }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -11,10 +11,10 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
|
|||||||
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 animate-pulse rounded-md bg-zinc-800" />
|
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-white/10" />
|
||||||
<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="h-4 w-1/2 animate-pulse rounded bg-zinc-800" />
|
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10" />
|
||||||
<span className="h-3 w-1/3 animate-pulse rounded bg-zinc-800" />
|
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -37,13 +37,9 @@ export function CreateStep1Screen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const download = async () => {
|
const download = async () => {
|
||||||
await writeTextFile(
|
await writeTextFile('lume-keys.txt', `Public key: ${npub}\nPrivate key: ${nsec}`, {
|
||||||
'lume-keys.txt',
|
dir: BaseDirectory.Download,
|
||||||
`Public key: ${pubkey}\nPrivate key: ${privkey}`,
|
});
|
||||||
{
|
|
||||||
dir: BaseDirectory.Download,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setDownloaded(true);
|
setDownloaded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,7 +85,7 @@ export function CreateStep1Screen() {
|
|||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
value={npub}
|
value={npub}
|
||||||
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@ -99,29 +95,21 @@ export function CreateStep1Screen() {
|
|||||||
readOnly
|
readOnly
|
||||||
type={privkeyInput}
|
type={privkeyInput}
|
||||||
value={nsec}
|
value={nsec}
|
||||||
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
className="relative h-11 w-full rounded-lg bg-white/10 py-1 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<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-white/10"
|
||||||
>
|
>
|
||||||
{privkeyInput === 'password' ? (
|
{privkeyInput === 'password' ? (
|
||||||
<EyeOffIcon
|
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-500 group-hover:text-white"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<EyeOnIcon
|
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-500 group-hover:text-white"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-sm text-zinc-500">
|
<div className="mt-2 text-sm text-white/50">
|
||||||
<p>
|
<p>
|
||||||
Your private key is your password. If you lose this key, you will lose
|
Your private key is your password. If you lose this key, you will lose
|
||||||
access to your account! Copy it and keep it in a safe place. There is no way
|
access to your account! Copy it and keep it in a safe place. There is no way
|
||||||
@ -138,7 +126,9 @@ export function CreateStep1Screen() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{downloaded ? (
|
{downloaded ? (
|
||||||
<span className="text-sm text-white/50">Saved in download folder</span>
|
<span className="inline-flex h-11 w-full items-center justify-center text-sm text-white/50">
|
||||||
|
Saved in Download folder
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Button preset="large-alt" onClick={() => download()}>
|
<Button preset="large-alt" onClick={() => download()}>
|
||||||
Download
|
Download
|
||||||
|
@ -85,29 +85,21 @@ export function CreateStep2Screen() {
|
|||||||
<input
|
<input
|
||||||
{...register('password', { required: true })}
|
{...register('password', { required: true })}
|
||||||
type={passwordInput}
|
type={passwordInput}
|
||||||
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-center text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showPassword()}
|
onClick={() => showPassword()}
|
||||||
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-white/10"
|
||||||
>
|
>
|
||||||
{passwordInput === 'password' ? (
|
{passwordInput === 'password' ? (
|
||||||
<EyeOffIcon
|
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-500 group-hover:text-white"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<EyeOnIcon
|
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-500 group-hover:text-white"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-zinc-500">
|
<div className="text-sm text-white/50">
|
||||||
<p>
|
<p>
|
||||||
Password is use to secure your key store in local machine, when you move
|
Password is use to secure your key store in local machine, when you move
|
||||||
to other clients, you just need to copy your private key as nsec or
|
to other clients, you just need to copy your private key as nsec or
|
||||||
@ -122,10 +114,10 @@ export function CreateStep2Screen() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
) : (
|
) : (
|
||||||
'Continue →'
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
|
@ -47,20 +47,10 @@ export function CreateStep3Screen() {
|
|||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="text-xl font-semibold text-white">Create your profile</h1>
|
<h1 className="text-xl font-semibold text-white">Create your profile</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
<div className="w-full overflow-hidden rounded-xl bg-white/10">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||||
<input
|
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||||
type={'hidden'}
|
<input type={'hidden'} {...register('banner')} value={banner} />
|
||||||
{...register('picture')}
|
|
||||||
value={picture}
|
|
||||||
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-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type={'hidden'}
|
|
||||||
{...register('banner')}
|
|
||||||
value={banner}
|
|
||||||
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-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative h-44 w-full bg-zinc-800">
|
<div className="relative h-44 w-full bg-zinc-800">
|
||||||
<Image
|
<Image
|
||||||
@ -79,7 +69,7 @@ export function CreateStep3Screen() {
|
|||||||
src={picture}
|
src={picture}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt="user's avatar"
|
alt="user's avatar"
|
||||||
className="h-14 w-14 rounded-lg object-cover ring-2 ring-zinc-900"
|
className="h-14 w-14 rounded-lg object-cover ring-2 ring-white/10"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
<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} />
|
||||||
@ -102,7 +92,7 @@ export function CreateStep3Screen() {
|
|||||||
minLength: 4,
|
minLength: 4,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@ -115,7 +105,7 @@ export function CreateStep3Screen() {
|
|||||||
<textarea
|
<textarea
|
||||||
{...register('about')}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-20 w-full resize-none rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@ -131,16 +121,16 @@ export function CreateStep3Screen() {
|
|||||||
required: false,
|
required: false,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
) : (
|
) : (
|
||||||
'Continue →'
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
|
@ -56,7 +56,7 @@ export function CreateStep4Screen() {
|
|||||||
<h1 className="text-xl font-semibold text-white">Create your Lume ID</h1>
|
<h1 className="text-xl font-semibold text-white">Create your Lume ID</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-4">
|
<div className="flex w-full flex-col items-center justify-center gap-4">
|
||||||
<div className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-zinc-800">
|
<div className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-white/10">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
@ -65,7 +65,7 @@ export function CreateStep4Screen() {
|
|||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder="satoshi"
|
placeholder="satoshi"
|
||||||
className="relative w-full bg-transparent py-3 pl-3.5 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-11 w-full bg-transparent py-1 pl-3.5 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<span className="pr-3.5 font-semibold text-fuchsia-500">@lume.nu</span>
|
<span className="pr-3.5 font-semibold text-fuchsia-500">@lume.nu</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -138,7 +138,7 @@ export function CreateStep5Screen() {
|
|||||||
|
|
||||||
const update = useMutation({
|
const update = useMutation({
|
||||||
mutationFn: (follows: string[]) => {
|
mutationFn: (follows: string[]) => {
|
||||||
return updateAccount('follows', follows, account.pubkey);
|
return updateAccount('follows', follows);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
|
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
|
||||||
@ -171,8 +171,8 @@ export function CreateStep5Screen() {
|
|||||||
<h1 className="text-xl font-semibold text-white">Personalized your newsfeed</h1>
|
<h1 className="text-xl font-semibold text-white">Personalized your newsfeed</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
<div className="w-full overflow-hidden rounded-xl bg-white/10">
|
||||||
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-white/50">
|
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-white/10 px-4 text-base font-medium text-white/50">
|
||||||
Follow at least
|
Follow at least
|
||||||
<span className="font-semibold text-fuchsia-500">
|
<span className="font-semibold text-fuchsia-500">
|
||||||
{follows.length}/10
|
{follows.length}/10
|
||||||
@ -181,7 +181,7 @@ export function CreateStep5Screen() {
|
|||||||
</div>
|
</div>
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<div className="inline-flex h-11 w-full items-center justify-center px-4 py-2">
|
<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-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
</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">
|
||||||
@ -190,7 +190,7 @@ export function CreateStep5Screen() {
|
|||||||
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-white/10 px-4 py-2 hover:bg-white/20 active:translate-y-1"
|
||||||
>
|
>
|
||||||
<User pubkey={item.pubkey} fallback={item.profile?.content} />
|
<User pubkey={item.pubkey} fallback={item.profile?.content} />
|
||||||
{follows.includes(item.pubkey) && (
|
{follows.includes(item.pubkey) && (
|
||||||
@ -210,7 +210,7 @@ export function CreateStep5Screen() {
|
|||||||
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
) : (
|
) : (
|
||||||
'Finish →'
|
'Finish →'
|
||||||
)}
|
)}
|
||||||
|
@ -101,14 +101,14 @@ export function ImportStep1Screen() {
|
|||||||
<h1 className="text-xl font-semibold text-white">Import your key</h1>
|
<h1 className="text-xl font-semibold text-white">Import your key</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-base font-semibold text-white/50">Private key</span>
|
<span className="text-base font-semibold text-white/50">Private key</span>
|
||||||
<input
|
<input
|
||||||
{...register('privkey', { required: true, minLength: 32 })}
|
{...register('privkey', { required: true, minLength: 32 })}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
placeholder="nsec or hexstring"
|
placeholder="nsec or hexstring"
|
||||||
className="relative w-full rounded-lg bg-zinc-800 px-3 py-3 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-red-400">
|
<span className="text-sm text-red-400">
|
||||||
{errors.privkey && <p>{errors.privkey.message}</p>}
|
{errors.privkey && <p>{errors.privkey.message}</p>}
|
||||||
@ -118,10 +118,10 @@ export function ImportStep1Screen() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
) : (
|
) : (
|
||||||
'Continue →'
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
|
@ -85,29 +85,21 @@ export function ImportStep2Screen() {
|
|||||||
<input
|
<input
|
||||||
{...register('password', { required: true })}
|
{...register('password', { required: true })}
|
||||||
type={passwordInput}
|
type={passwordInput}
|
||||||
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-center text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showPassword()}
|
onClick={() => showPassword()}
|
||||||
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-white/10"
|
||||||
>
|
>
|
||||||
{passwordInput === 'password' ? (
|
{passwordInput === 'password' ? (
|
||||||
<EyeOffIcon
|
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-500 group-hover:text-white"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<EyeOnIcon
|
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-500 group-hover:text-white"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-zinc-500">
|
<div className="text-sm text-white/50">
|
||||||
<p>
|
<p>
|
||||||
Password is use to unlock app and secure your key store in local machine.
|
Password is use to unlock app and secure your key store in local machine.
|
||||||
When you move to other clients, you just need to copy your private key as
|
When you move to other clients, you just need to copy your private key as
|
||||||
@ -122,10 +114,10 @@ export function ImportStep2Screen() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
) : (
|
) : (
|
||||||
'Continue →'
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
|
@ -1,54 +1,38 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { User } from '@app/auth/components/user';
|
import { User } from '@app/auth/components/user';
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
import { updateLastLogin } from '@libs/storage';
|
||||||
import { updateAccount } from '@libs/storage';
|
|
||||||
|
|
||||||
import { Button } from '@shared/button';
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
import { setToArray } from '@utils/transform';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
export function ImportStep3Screen() {
|
export function ImportStep3Screen() {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { status, account } = useAccount();
|
const { status, account } = useAccount();
|
||||||
|
const { fetchNotes, fetchChats } = useNostr();
|
||||||
const update = useMutation({
|
|
||||||
mutationFn: (follows: string[]) => {
|
|
||||||
return updateAccount('follows', follows, account.pubkey);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
// show loading indicator
|
// show loading indicator
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const user = ndk.getUser({ hexpubkey: account.pubkey });
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const follows = await user.follows();
|
await fetchNotes();
|
||||||
|
await fetchChats();
|
||||||
|
await updateLastLogin(now);
|
||||||
|
|
||||||
// follows as list
|
navigate('/', { replace: true });
|
||||||
const followsList = setToArray(follows);
|
} catch (e) {
|
||||||
|
console.log('error: ', e);
|
||||||
// update
|
setLoading(false);
|
||||||
update.mutate([...followsList, account.pubkey]);
|
|
||||||
|
|
||||||
// redirect to next step
|
|
||||||
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
|
|
||||||
} catch {
|
|
||||||
console.log('error');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,30 +40,42 @@ export function ImportStep3Screen() {
|
|||||||
<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 ? 'Prefetching data...' : '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 bg-white/10 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-white/10" />
|
||||||
<div>
|
<div>
|
||||||
<div 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-white/10" />
|
||||||
<div className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
|
<div className="h-3 w-36 animate-pulse rounded bg-white/10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<User pubkey={account.pubkey} />
|
<User pubkey={account.pubkey} />
|
||||||
<Button preset="large" onClick={() => submit()}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
|
onClick={() => submit()}
|
||||||
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>It might take a bit, please patient...</span>
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Continue →'
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Continue</span>
|
||||||
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,13 +135,13 @@ export function MigrateScreen() {
|
|||||||
<EyeOffIcon
|
<EyeOffIcon
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className="text-zinc-500 group-hover:text-white"
|
className="text-white/50 group-hover:text-white"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EyeOnIcon
|
<EyeOnIcon
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className="text-zinc-500 group-hover:text-white"
|
className="text-white/50 group-hover:text-white"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,18 +1,39 @@
|
|||||||
|
import { LogicalSize, appWindow } from '@tauri-apps/plugin-window';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
|
||||||
export function WelcomeScreen() {
|
export function WelcomeScreen() {
|
||||||
|
useEffect(() => {
|
||||||
|
async function setWindow() {
|
||||||
|
await appWindow.setSize(new LogicalSize(400, 500));
|
||||||
|
await appWindow.setResizable(false);
|
||||||
|
}
|
||||||
|
setWindow();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
appWindow.setSize(new LogicalSize(1080, 800)).then(() => {
|
||||||
|
appWindow.setResizable(false);
|
||||||
|
appWindow.center();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col justify-between">
|
<div className="flex h-screen w-full flex-col justify-between bg-white/10">
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-col gap-10 pt-16">
|
||||||
<h1 className="text-5xl font-semibold">Have fun together!</h1>
|
<div className="flex flex-col gap-2 text-center">
|
||||||
</div>
|
<h1 className="text-3xl font-medium text-white">Welcome to Lume</h1>
|
||||||
<div className="flex flex-1 items-end justify-center">
|
<h3 className="mx-auto w-2/3 text-white/50">
|
||||||
<div className="inline-flex w-full flex-col gap-3 px-10 pb-10">
|
Let's get you up and connecting with all peoples around the world on
|
||||||
|
Nostr
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex w-full flex-col items-center gap-3 px-4 pb-10">
|
||||||
<Link
|
<Link
|
||||||
to="/auth/import"
|
to="/auth/import"
|
||||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-white hover:bg-fuchsia-600"
|
className="inline-flex h-11 w-2/3 items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Login with private key</span>
|
<span>Login with private key</span>
|
||||||
@ -20,12 +41,15 @@ export function WelcomeScreen() {
|
|||||||
</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 bg-zinc-800 px-6 font-medium text-zinc-200 hover:bg-zinc-700"
|
className="inline-flex h-11 w-2/3 items-center justify-center gap-2 rounded-lg bg-white/10 px-6 font-medium leading-none text-zinc-200 hover:bg-white/20 focus:outline-none"
|
||||||
>
|
>
|
||||||
Create new key
|
Create new key
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-1 items-end justify-center pb-10">
|
||||||
|
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ export function ChannelCreateModal() {
|
|||||||
// close modal
|
// close modal
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
// redirect to channel page
|
// redirect to channel page
|
||||||
navigate(`/app/channel/${event.id}`);
|
navigate(`/channel/${event.id}`);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error: ', e);
|
console.log('error: ', e);
|
||||||
@ -112,7 +112,7 @@ export function ChannelCreateModal() {
|
|||||||
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
|
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
|
||||||
>
|
>
|
||||||
<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">
|
||||||
<PlusIcon width={12} height={12} className="text-zinc-500" />
|
<PlusIcon width={12} height={12} className="text-white/50" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="font-medium text-white/50">Create channel</h5>
|
<h5 className="font-medium text-white/50">Create channel</h5>
|
||||||
@ -174,7 +174,7 @@ export function ChannelCreateModal() {
|
|||||||
type={'hidden'}
|
type={'hidden'}
|
||||||
{...register('picture')}
|
{...register('picture')}
|
||||||
value={image}
|
value={image}
|
||||||
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-white/50 dark:bg-zinc-800 dark:text-white 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-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm font-medium uppercase tracking-wider text-white/50">
|
<span className="text-sm font-medium uppercase tracking-wider text-white/50">
|
||||||
@ -206,7 +206,7 @@ export function ChannelCreateModal() {
|
|||||||
minLength: 4,
|
minLength: 4,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@ -219,7 +219,7 @@ export function ChannelCreateModal() {
|
|||||||
<textarea
|
<textarea
|
||||||
{...register('about')}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
|
@ -7,7 +7,7 @@ export function ChannelsListItem({ data }: { data: any }) {
|
|||||||
const channel = useChannelProfile(data.event_id);
|
const channel = useChannelProfile(data.event_id);
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/app/channel/${data.event_id}`}
|
to={`/channel/${data.event_id}`}
|
||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
|
@ -95,10 +95,10 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
|||||||
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 bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500`}
|
} w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-white/50`}
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-0 right-2 h-11">
|
<div className="absolute bottom-0 right-2 h-11">
|
||||||
<div className="flex h-full items-center justify-end gap-3 text-zinc-500">
|
<div className="flex h-full items-center justify-end gap-3 text-white/50">
|
||||||
<MediaUploader setState={setValue} />
|
<MediaUploader setState={setValue} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -13,7 +13,7 @@ export function UserReply({ pubkey }: { pubkey: string }) {
|
|||||||
{isError || isLoading ? (
|
{isError || isLoading ? (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-9 w-9 shrink animate-pulse overflow-hidden rounded bg-zinc-800" />
|
<div className="relative h-9 w-9 shrink animate-pulse overflow-hidden rounded bg-zinc-800" />
|
||||||
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-base font-medium leading-none text-zinc-500" />
|
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-base font-medium leading-none text-white/50" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -25,7 +25,7 @@ export function UserReply({ pubkey }: { pubkey: string }) {
|
|||||||
className="h-9 w-9 rounded object-cover"
|
className="h-9 w-9 rounded object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="max-w-[10rem] truncate text-sm font-medium leading-none text-zinc-500">
|
<span className="max-w-[10rem] truncate text-sm font-medium leading-none text-white/50">
|
||||||
Replying to {user?.name || shortenKey(pubkey)}
|
Replying to {user?.name || shortenKey(pubkey)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
|
@ -23,7 +23,7 @@ export function ChatsListItem({ data }: { data: Chats }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/app/chats/${data.sender_pubkey}`}
|
to={`/chats/${data.sender_pubkey}`}
|
||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
|
@ -14,11 +14,9 @@ export function NewMessageModal() {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { status, account } = useAccount();
|
const { status, account } = useAccount();
|
||||||
|
|
||||||
const follows = account ? JSON.parse(account.follows as string) : [];
|
|
||||||
|
|
||||||
const openChat = (pubkey: string) => {
|
const openChat = (pubkey: string) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
navigate(`/app/chats/${pubkey}`);
|
navigate(`/chats/${pubkey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,7 +59,7 @@ export function NewMessageModal() {
|
|||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
follows.map((follow) => (
|
account?.follows?.map((follow) => (
|
||||||
<div
|
<div
|
||||||
key={follow}
|
key={follow}
|
||||||
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
|
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
|
||||||
|
@ -24,7 +24,7 @@ export function ChatsListSelfItem({ data }: { data: { pubkey: string } }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/app/chats/${data.pubkey}`}
|
to={`/chats/${data.pubkey}`}
|
||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
|
@ -33,7 +33,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="leading-tight">{user?.bio || user?.about}</p>
|
<p className="leading-tight">{user?.bio || user?.about}</p>
|
||||||
<Link
|
<Link
|
||||||
to={`/app/users/${pubkey}`}
|
to={`/users/${pubkey}`}
|
||||||
className="mt-3 inline-flex h-10 w-full items-center justify-center rounded-md bg-white/10 text-sm font-medium text-white hover:bg-fuchsia-500"
|
className="mt-3 inline-flex h-10 w-full items-center justify-center rounded-md bg-white/10 text-sm font-medium text-white hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
View full profile
|
View full profile
|
||||||
|
@ -15,7 +15,7 @@ export function UnknownsModal({ data }: { data: Chats[] }) {
|
|||||||
|
|
||||||
const openChat = (pubkey: string) => {
|
const openChat = (pubkey: string) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
navigate(`/app/chats/${pubkey}`);
|
navigate(`/chats/${pubkey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
169
src/app/root.tsx
169
src/app/root.tsx
@ -1,169 +0,0 @@
|
|||||||
import { NDKUser } from '@nostr-dev-kit/ndk';
|
|
||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import {
|
|
||||||
countTotalNotes,
|
|
||||||
createChat,
|
|
||||||
createNote,
|
|
||||||
getLastLogin,
|
|
||||||
updateAccount,
|
|
||||||
updateLastLogin,
|
|
||||||
} from '@libs/storage';
|
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { nHoursAgo } from '@utils/date';
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
const totalNotes = await countTotalNotes();
|
|
||||||
const lastLogin = await getLastLogin();
|
|
||||||
|
|
||||||
export function Root() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { ndk, relayUrls, fetcher } = useNDK();
|
|
||||||
const { status, account } = useAccount();
|
|
||||||
|
|
||||||
async function fetchNetwork() {
|
|
||||||
const network = new Set<string>();
|
|
||||||
|
|
||||||
// fetch user's follows
|
|
||||||
const user = ndk.getUser({ hexpubkey: account.pubkey });
|
|
||||||
const follows = await user.follows();
|
|
||||||
follows.forEach((follow: NDKUser) => {
|
|
||||||
network.add(nip19.decode(follow.npub).data as string);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update user's follows in db
|
|
||||||
await updateAccount('follows', [...network]);
|
|
||||||
|
|
||||||
// fetch network
|
|
||||||
for (const item of network) {
|
|
||||||
const user = ndk.getUser({ hexpubkey: item });
|
|
||||||
const follows = await user.follows();
|
|
||||||
follows.forEach((follow: NDKUser) => {
|
|
||||||
network.add(nip19.decode(follow.npub).data as string);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// update user's network in db
|
|
||||||
await updateAccount('network', [...network]);
|
|
||||||
|
|
||||||
return [...network];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchNotes() {
|
|
||||||
try {
|
|
||||||
const network = await fetchNetwork();
|
|
||||||
|
|
||||||
if (network.length > 0) {
|
|
||||||
let since: number;
|
|
||||||
if (totalNotes === 0 || lastLogin === 0) {
|
|
||||||
since = nHoursAgo(48);
|
|
||||||
} else {
|
|
||||||
since = lastLogin;
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = fetcher.allEventsIterator(
|
|
||||||
relayUrls,
|
|
||||||
{ kinds: [1], authors: network },
|
|
||||||
{ since: since },
|
|
||||||
{ skipVerification: true }
|
|
||||||
);
|
|
||||||
for await (const event of events) {
|
|
||||||
await createNote(
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.kind,
|
|
||||||
event.tags,
|
|
||||||
event.content,
|
|
||||||
event.created_at
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.log('error: ', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchChats() {
|
|
||||||
try {
|
|
||||||
const sendMessages = await fetcher.fetchAllEvents(
|
|
||||||
relayUrls,
|
|
||||||
{
|
|
||||||
kinds: [4],
|
|
||||||
authors: [account.pubkey],
|
|
||||||
},
|
|
||||||
{ since: lastLogin }
|
|
||||||
);
|
|
||||||
|
|
||||||
const receiveMessages = await fetcher.fetchAllEvents(
|
|
||||||
relayUrls,
|
|
||||||
{
|
|
||||||
kinds: [4],
|
|
||||||
'#p': [account.pubkey],
|
|
||||||
},
|
|
||||||
{ since: lastLogin }
|
|
||||||
);
|
|
||||||
|
|
||||||
const events = [...sendMessages, ...receiveMessages];
|
|
||||||
for (const event of events) {
|
|
||||||
const receiverPubkey = event.tags.find((t) => t[0] === 'p')[1] || account.pubkey;
|
|
||||||
await createChat(
|
|
||||||
event.id,
|
|
||||||
receiverPubkey,
|
|
||||||
event.pubkey,
|
|
||||||
event.content,
|
|
||||||
event.tags,
|
|
||||||
event.created_at
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.log('error: ', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function prefetch() {
|
|
||||||
const notes = await fetchNotes();
|
|
||||||
const chats = await fetchChats();
|
|
||||||
if (notes && chats) {
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
await updateLastLogin(now);
|
|
||||||
navigate('/app/space', { replace: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'success' && account) {
|
|
||||||
prefetch();
|
|
||||||
}
|
|
||||||
}, [status]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-screen w-screen bg-black/90">
|
|
||||||
<div className="flex h-screen w-full flex-col">
|
|
||||||
<div data-tauri-drag-region className="h-11 shrink-0" />
|
|
||||||
<div className="relative flex min-h-0 w-full flex-1 items-center justify-center">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-4">
|
|
||||||
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-lg font-semibold leading-tight text-white">
|
|
||||||
Prefetching data...
|
|
||||||
</h3>
|
|
||||||
<p className="text-white/50">
|
|
||||||
This may take a few seconds, please don't close app.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -72,13 +72,13 @@ export function AccountSettingsScreen() {
|
|||||||
<EyeOffIcon
|
<EyeOffIcon
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className="text-zinc-500 group-hover:text-white"
|
className="text-white/50 group-hover:text-white"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EyeOnIcon
|
<EyeOnIcon
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className="text-zinc-500 group-hover:text-white"
|
className="text-white/50 group-hover:text-white"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
@ -15,10 +15,10 @@ export function ShortcutsSettingsScreen() {
|
|||||||
</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 width={12} height={12} className="text-zinc-500" />
|
<CommandIcon width={12} height={12} className="text-white/50" />
|
||||||
</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-sm leading-none text-zinc-500">N</span>
|
<span className="text-sm leading-none text-white/50">N</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -30,10 +30,10 @@ export function ShortcutsSettingsScreen() {
|
|||||||
</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 width={12} height={12} className="text-zinc-500" />
|
<CommandIcon width={12} height={12} className="text-white/50" />
|
||||||
</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-sm leading-none text-zinc-500">I</span>
|
<span className="text-sm leading-none text-white/50">I</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -45,10 +45,10 @@ export function ShortcutsSettingsScreen() {
|
|||||||
</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 width={12} height={12} className="text-zinc-500" />
|
<CommandIcon width={12} height={12} className="text-white/50" />
|
||||||
</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-sm leading-none text-zinc-500">F</span>
|
<span className="text-sm leading-none text-white/50">F</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -60,10 +60,10 @@ export function ShortcutsSettingsScreen() {
|
|||||||
</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 width={12} height={12} className="text-zinc-500" />
|
<CommandIcon width={12} height={12} className="text-white/50" />
|
||||||
</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-sm leading-none text-zinc-500">P</span>
|
<span className="text-sm leading-none text-white/50">P</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -75,10 +75,10 @@ export function ShortcutsSettingsScreen() {
|
|||||||
</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 width={12} height={12} className="text-zinc-500" />
|
<CommandIcon width={12} height={12} className="text-white/50" />
|
||||||
</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-sm leading-none text-zinc-500">B</span>
|
<span className="text-sm leading-none text-white/50">B</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -148,7 +148,7 @@ export function NetworkBlock() {
|
|||||||
Follow more people to have more fun.
|
Follow more people to have more fun.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/app/trending"
|
to="/trending"
|
||||||
className="inline-flex w-max rounded bg-fuchsia-500 px-2.5 py-1.5 text-sm hover:bg-fuchsia-600"
|
className="inline-flex w-max rounded bg-fuchsia-500 px-2.5 py-1.5 text-sm hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
Trending
|
Trending
|
||||||
|
@ -169,7 +169,7 @@ export function FeedModal() {
|
|||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
JSON.parse(account.follows as string).map((follow) => (
|
account?.follows?.map((follow) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={follow}
|
key={follow}
|
||||||
value={follow}
|
value={follow}
|
||||||
|
@ -19,11 +19,9 @@ export function useNewsfeed() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'success' && account) {
|
if (status === 'success' && account) {
|
||||||
const follows = account ? JSON.parse(account.follows as string) : [];
|
|
||||||
|
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [1, 6],
|
kinds: [1, 6],
|
||||||
authors: follows,
|
authors: account.follows,
|
||||||
since: now.current,
|
since: now.current,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
58
src/app/splash.tsx
Normal file
58
src/app/splash.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { platform } from '@tauri-apps/plugin-os';
|
||||||
|
import { appWindow } from '@tauri-apps/plugin-window';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { getActiveAccount, updateLastLogin } from '@libs/storage';
|
||||||
|
|
||||||
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
|
const account = await getActiveAccount();
|
||||||
|
const osPlatform = await platform();
|
||||||
|
|
||||||
|
if (osPlatform !== 'macos') {
|
||||||
|
appWindow.setDecorations(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SplashScreen() {
|
||||||
|
const { fetchChats, fetchNotes } = useNostr();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function prefetch() {
|
||||||
|
const notes = await fetchNotes();
|
||||||
|
const chats = await fetchChats();
|
||||||
|
if (notes && chats) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
await updateLastLogin(now);
|
||||||
|
invoke('close_splashscreen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
prefetch();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
setTimeout(() => invoke('close_splashscreen'), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-screen w-screen items-center justify-center bg-black">
|
||||||
|
<div data-tauri-drag-region className="absolute left-0 top-0 z-10 h-11 w-full" />
|
||||||
|
<div className="flex min-h-0 w-full flex-1 items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4">
|
||||||
|
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
||||||
|
<div className="flex flex-col gap-1 text-center">
|
||||||
|
<h3 className="font-semibold leading-none text-white">Prefetching data</h3>
|
||||||
|
<p className="text-sm leading-none text-white/50">
|
||||||
|
This may take a few seconds, please don't close app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -73,7 +73,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
<h5 className="text-lg font-semibold 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] truncate text-sm leading-none text-zinc-500">
|
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
|
||||||
{user?.nip05 || shortenKey(pubkey)}
|
{user?.nip05 || shortenKey(pubkey)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -103,7 +103,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to={`/app/chats/${pubkey}`}
|
to={`/chats/${pubkey}`}
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
|
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
|
||||||
|
@ -45,7 +45,7 @@ export function UserScreen() {
|
|||||||
<div className="mt-8 h-full w-full border-t border-zinc-900">
|
<div className="mt-8 h-full w-full border-t border-zinc-900">
|
||||||
<div className="flex flex-col justify-start gap-1 px-3 pt-4 text-start">
|
<div className="flex flex-col justify-start gap-1 px-3 pt-4 text-start">
|
||||||
<p className="text-lg font-semibold leading-none text-zinc-200">Latest posts</p>
|
<p className="text-lg font-semibold leading-none text-zinc-200">Latest posts</p>
|
||||||
<span className="text-sm leading-none text-zinc-500">48 hours ago</span>
|
<span className="text-sm leading-none text-white/50">48 hours ago</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full max-w-[400px] flex-col justify-between gap-1.5 pb-4 pt-1.5">
|
<div className="flex h-full max-w-[400px] flex-col justify-between gap-1.5 pb-4 pt-1.5">
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
|
@ -24,6 +24,8 @@ export async function getActiveAccount() {
|
|||||||
'SELECT * FROM accounts WHERE is_active = 1;'
|
'SELECT * FROM accounts WHERE is_active = 1;'
|
||||||
);
|
);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
|
result[0].follows = destr(result[0].follows);
|
||||||
|
result[0].network = destr(result[0].network);
|
||||||
return result[0];
|
return result[0];
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@ -302,8 +304,6 @@ export async function getChannelUsers(channel_id: string) {
|
|||||||
export async function getChats() {
|
export async function getChats() {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const account = await getActiveAccount();
|
const account = await getActiveAccount();
|
||||||
const follows =
|
|
||||||
typeof account.follows === 'string' ? JSON.parse(account.follows) : account.follows;
|
|
||||||
|
|
||||||
const chats: { follows: Array<Chats> | null; unknowns: Array<Chats> | null } = {
|
const chats: { follows: Array<Chats> | null; unknowns: Array<Chats> | null } = {
|
||||||
follows: [],
|
follows: [],
|
||||||
@ -318,7 +318,7 @@ export async function getChats() {
|
|||||||
result = result.sort((a, b) => a.new_messages - b.new_messages);
|
result = result.sort((a, b) => a.new_messages - b.new_messages);
|
||||||
|
|
||||||
chats.follows = result.filter((el) => {
|
chats.follows = result.filter((el) => {
|
||||||
return follows.some((i) => {
|
return account.follows.some((i) => {
|
||||||
return i === el.sender_pubkey;
|
return i === el.sender_pubkey;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -92,7 +92,7 @@ export function ActiveAccount({ data }: { data: { pubkey: string; npub: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/app/users/${data.pubkey}`} className="relative inline-block h-9 w-9">
|
<Link to={`/users/${data.pubkey}`} className="relative inline-block h-9 w-9">
|
||||||
<Image
|
<Image
|
||||||
src={user?.picture || user?.image}
|
src={user?.picture || user?.image}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { LogicalSize, appWindow } from '@tauri-apps/plugin-window';
|
|
||||||
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
||||||
|
|
||||||
import { Navigation } from '@shared/navigation';
|
import { Navigation } from '@shared/navigation';
|
||||||
|
|
||||||
await appWindow.setSize(new LogicalSize(1080, 800));
|
|
||||||
|
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen">
|
<div className="flex h-screen w-screen">
|
||||||
@ -13,7 +10,11 @@ export function AppLayout() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full flex-1 bg-black/90">
|
<div className="h-full w-full flex-1 bg-black/90">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration
|
||||||
|
getKey={(location) => {
|
||||||
|
return location.pathname;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -24,11 +24,11 @@ export function Button({
|
|||||||
break;
|
break;
|
||||||
case 'large':
|
case 'large':
|
||||||
preClass =
|
preClass =
|
||||||
'h-11 w-full bg-fuchsia-500 rounded-md font-medium text-white hover:bg-fuchsia-600';
|
'h-11 w-full bg-fuchsia-500 rounded-lg font-medium text-white hover:bg-fuchsia-600';
|
||||||
break;
|
break;
|
||||||
case 'large-alt':
|
case 'large-alt':
|
||||||
preClass =
|
preClass =
|
||||||
'h-11 w-full bg-zinc-800 rounded-md font-medium text-white border-t border-zinc-700/50 hover:bg-zinc-900';
|
'h-11 w-full bg-white/10 rounded-lg font-medium text-white hover:bg-white/20';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -40,7 +40,7 @@ export function Button({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'inline-flex transform items-center justify-center gap-1 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
|
'inline-flex transform items-center justify-center gap-1 leading-none focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
|
||||||
preClass
|
preClass
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -197,13 +197,13 @@ export function EditProfileModal() {
|
|||||||
type={'hidden'}
|
type={'hidden'}
|
||||||
{...register('picture')}
|
{...register('picture')}
|
||||||
value={picture}
|
value={picture}
|
||||||
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-white/50 dark:bg-zinc-800 dark:text-white 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-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type={'hidden'}
|
type={'hidden'}
|
||||||
{...register('banner')}
|
{...register('banner')}
|
||||||
value={banner}
|
value={banner}
|
||||||
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-white/50 dark:bg-zinc-800 dark:text-white 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-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative h-44 w-full bg-zinc-800">
|
<div className="relative h-44 w-full bg-zinc-800">
|
||||||
@ -246,7 +246,7 @@ export function EditProfileModal() {
|
|||||||
minLength: 4,
|
minLength: 4,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@ -263,7 +263,7 @@ export function EditProfileModal() {
|
|||||||
minLength: 4,
|
minLength: 4,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
|
||||||
{nip05.verified ? (
|
{nip05.verified ? (
|
||||||
@ -295,7 +295,7 @@ export function EditProfileModal() {
|
|||||||
<textarea
|
<textarea
|
||||||
{...register('about')}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@ -309,7 +309,7 @@ export function EditProfileModal() {
|
|||||||
type={'text'}
|
type={'text'}
|
||||||
{...register('website', { required: false })}
|
{...register('website', { required: false })}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@ -323,7 +323,7 @@ export function EditProfileModal() {
|
|||||||
type={'text'}
|
type={'text'}
|
||||||
{...register('lud16', { required: false })}
|
{...register('lud16', { required: false })}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-zinc-500"
|
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -64,7 +64,7 @@ export function Navigation() {
|
|||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/app/space"
|
to="/"
|
||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
@ -79,7 +79,7 @@ export function Navigation() {
|
|||||||
<span className="font-medium">Spaces</span>
|
<span className="font-medium">Spaces</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/app/trending"
|
to="/trending"
|
||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
|
@ -43,7 +43,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
<Popover.Content className="w-[200px] overflow-hidden rounded-md bg-white/10 backdrop-blur-xl focus:outline-none">
|
<Popover.Content className="w-[200px] overflow-hidden rounded-md bg-white/10 backdrop-blur-xl focus:outline-none">
|
||||||
<div className="flex flex-col p-2">
|
<div className="flex flex-col p-2">
|
||||||
<Link
|
<Link
|
||||||
to={`/app/events/${id}`}
|
to={`/events/${id}`}
|
||||||
className="inline-flex h-10 items-center rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
className="inline-flex h-10 items-center rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Open as new screen
|
Open as new screen
|
||||||
@ -63,7 +63,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
Copy ID
|
Copy ID
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
to={`/app/users/${pubkey}`}
|
to={`/users/${pubkey}`}
|
||||||
className="inline-flex h-10 items-center rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
className="inline-flex h-10 items-center rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||||
>
|
>
|
||||||
View profile
|
View profile
|
||||||
|
@ -8,23 +8,23 @@ import { usePublish } from '@utils/hooks/usePublish';
|
|||||||
const REACTIONS = [
|
const REACTIONS = [
|
||||||
{
|
{
|
||||||
content: '👏',
|
content: '👏',
|
||||||
img: '/public/clapping_hands.png',
|
img: '/clapping_hands.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: '🤪',
|
content: '🤪',
|
||||||
img: '/public/face_with_tongue.png',
|
img: '/face_with_tongue.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: '😮',
|
content: '😮',
|
||||||
img: '/public/face_with_open_mouth.png',
|
img: '/face_with_open_mouth.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: '😢',
|
content: '😢',
|
||||||
img: '/public/crying_face.png',
|
img: '/crying_face.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: '🤡',
|
content: '🤡',
|
||||||
img: '/public/clown_face.png',
|
img: '/clown_face.png',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -82,11 +82,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
onClick={() => react('👏')}
|
onClick={() => react('👏')}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded hover:bg-white/10"
|
className="inline-flex h-8 w-8 items-center justify-center rounded hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<img
|
<img src="/clapping_hands.png" alt="Clapping Hands" className="h-6 w-6" />
|
||||||
src="/public/clapping_hands.png"
|
|
||||||
alt="Clapping Hands"
|
|
||||||
className="h-6 w-6"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -94,7 +90,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
|
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/public/face_with_tongue.png"
|
src="/face_with_tongue.png"
|
||||||
alt="Face with Tongue"
|
alt="Face with Tongue"
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
/>
|
/>
|
||||||
@ -105,7 +101,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
|
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/public/face_with_open_mouth.png"
|
src="/face_with_open_mouth.png"
|
||||||
alt="Face with Open Mouth"
|
alt="Face with Open Mouth"
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
/>
|
/>
|
||||||
@ -115,14 +111,14 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
onClick={() => react('😢')}
|
onClick={() => react('😢')}
|
||||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
|
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<img src="/public/crying_face.png" alt="Crying Face" className="h-6 w-6" />
|
<img src="/crying_face.png" alt="Crying Face" className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => react('🤡')}
|
onClick={() => react('🤡')}
|
||||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
|
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<img src="/public/clown_face.png" alt="Clown Face" className="h-6 w-6" />
|
<img src="/clown_face.png" alt="Clown Face" className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Popover.Arrow className="fill-black" />
|
<Popover.Arrow className="fill-black" />
|
||||||
|
@ -83,7 +83,7 @@ export function NotificationModal({ pubkey }: { pubkey: string }) {
|
|||||||
) : data.length < 1 ? (
|
) : data.length < 1 ? (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
<p className="mb-1 text-4xl">🎉</p>
|
<p className="mb-1 text-4xl">🎉</p>
|
||||||
<p className="font-medium text-zinc-500">
|
<p className="font-medium text-white/50">
|
||||||
Yo!, you've no new notifications
|
Yo!, you've no new notifications
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,7 @@ export function NotiRepost({ event }: { event: NDKEvent }) {
|
|||||||
<p className="leading-none text-white/50">repost your post</p>
|
<p className="leading-none text-white/50">repost your post</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="leading-none text-zinc-500">{createdAt}</span>
|
<span className="leading-none text-white/50">{createdAt}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="-mt-5 pl-[44px]">{root && <MentionNote id={root} />}</div>
|
<div className="-mt-5 pl-[44px]">{root && <MentionNote id={root} />}</div>
|
||||||
|
@ -121,13 +121,13 @@ export function User({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 px-3 py-3">
|
<div className="flex items-center gap-2 px-3 py-3">
|
||||||
<Link
|
<Link
|
||||||
to={`/app/users/${pubkey}`}
|
to={`/users/${pubkey}`}
|
||||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
|
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
View profile
|
View profile
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to={`/app/chats/${pubkey}`}
|
to={`/chats/${pubkey}`}
|
||||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
|
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
|
@ -97,7 +97,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to={`/app/chats/${pubkey}`}
|
to={`/chats/${pubkey}`}
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
|
135
src/utils/hooks/useNostr.tsx
Normal file
135
src/utils/hooks/useNostr.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { NDKUser } from '@nostr-dev-kit/ndk';
|
||||||
|
import { LRUCache } from 'lru-cache';
|
||||||
|
import { NostrEvent } from 'nostr-fetch';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
import {
|
||||||
|
countTotalNotes,
|
||||||
|
createChat,
|
||||||
|
createNote,
|
||||||
|
getActiveAccount,
|
||||||
|
getLastLogin,
|
||||||
|
updateAccount,
|
||||||
|
} from '@libs/storage';
|
||||||
|
|
||||||
|
import { nHoursAgo } from '@utils/date';
|
||||||
|
|
||||||
|
export function useNostr() {
|
||||||
|
const { ndk, relayUrls, fetcher } = useNDK();
|
||||||
|
|
||||||
|
async function fetchNetwork() {
|
||||||
|
const account = await getActiveAccount();
|
||||||
|
const follows = new Set<string>();
|
||||||
|
const lruNetwork = new LRUCache<string, string, void>({ max: 300 });
|
||||||
|
|
||||||
|
let network: string[];
|
||||||
|
|
||||||
|
// fetch user's follows
|
||||||
|
const user = ndk.getUser({ hexpubkey: account.pubkey });
|
||||||
|
const list = await user.follows();
|
||||||
|
list.forEach((item: NDKUser) => {
|
||||||
|
follows.add(nip19.decode(item.npub).data as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch network
|
||||||
|
if (!account.network) {
|
||||||
|
const events = await fetcher.fetchAllEvents(
|
||||||
|
relayUrls,
|
||||||
|
{ kinds: [3], authors: [...follows] },
|
||||||
|
{ since: 0 },
|
||||||
|
{ skipVerification: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
events.forEach((event: NostrEvent) => {
|
||||||
|
event.tags.forEach((tag) => {
|
||||||
|
if (tag[0] === 'p') lruNetwork.set(tag[1], tag[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
network = [...network.values()] as string[];
|
||||||
|
} else {
|
||||||
|
network = account.network;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update user in db
|
||||||
|
await updateAccount('follows', [...follows]);
|
||||||
|
await updateAccount('network', network);
|
||||||
|
|
||||||
|
return [...new Set([...follows, ...network])];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchNotes = async () => {
|
||||||
|
try {
|
||||||
|
const network = (await fetchNetwork()) as string[];
|
||||||
|
|
||||||
|
const totalNotes = await countTotalNotes();
|
||||||
|
const lastLogin = await getLastLogin();
|
||||||
|
|
||||||
|
if (network.length > 0) {
|
||||||
|
let since: number;
|
||||||
|
if (totalNotes === 0 || lastLogin === 0) {
|
||||||
|
since = nHoursAgo(6);
|
||||||
|
} else {
|
||||||
|
since = lastLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await fetcher.fetchAllEvents(
|
||||||
|
relayUrls,
|
||||||
|
{ kinds: [1], authors: network },
|
||||||
|
{ since: since },
|
||||||
|
{ skipVerification: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
await createNote(
|
||||||
|
event.id,
|
||||||
|
event.pubkey,
|
||||||
|
event.kind,
|
||||||
|
event.tags,
|
||||||
|
event.content,
|
||||||
|
event.created_at
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log('error: ', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchChats = async () => {
|
||||||
|
try {
|
||||||
|
const account = await getActiveAccount();
|
||||||
|
const lastLogin = await getLastLogin();
|
||||||
|
const incomingMessages = await fetcher.fetchAllEvents(
|
||||||
|
relayUrls,
|
||||||
|
{
|
||||||
|
kinds: [4],
|
||||||
|
'#p': [account.pubkey],
|
||||||
|
},
|
||||||
|
{ since: lastLogin },
|
||||||
|
{ skipVerification: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const event of incomingMessages) {
|
||||||
|
const receiverPubkey = event.tags.find((t) => t[0] === 'p')[1] || account.pubkey;
|
||||||
|
await createChat(
|
||||||
|
event.id,
|
||||||
|
receiverPubkey,
|
||||||
|
event.pubkey,
|
||||||
|
event.content,
|
||||||
|
event.tags,
|
||||||
|
event.created_at
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log('error: ', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { fetchNotes, fetchChats };
|
||||||
|
}
|
4
src/utils/types.d.ts
vendored
4
src/utils/types.d.ts
vendored
@ -20,8 +20,8 @@ export interface Account extends NDKUserProfile {
|
|||||||
id: number;
|
id: number;
|
||||||
npub: string;
|
npub: string;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
follows: string[] | string;
|
follows: string[];
|
||||||
network: string[] | string;
|
network: string[];
|
||||||
is_active: number;
|
is_active: number;
|
||||||
privkey?: string; // deprecated
|
privkey?: string; // deprecated
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user