wip: network

This commit is contained in:
Ren Amamiya 2023-08-06 07:59:43 +07:00
parent 373a0f0608
commit 71338b3b07
49 changed files with 465 additions and 424 deletions

View File

@ -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",

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -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");
} }

View File

@ -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
} }
} }

View File

@ -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 }}
/> />
); );

View File

@ -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>
); );

View File

@ -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

View File

@ -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 →'
)} )}

View File

@ -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 →'
)} )}

View File

@ -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>

View File

@ -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 →'
)} )}

View File

@ -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 →'
)} )}

View File

@ -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 →'
)} )}

View File

@ -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>

View File

@ -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>

View File

@ -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&apos;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>
); );
} }

View File

@ -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">

View File

@ -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(

View File

@ -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"

View File

@ -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>
</> </>

View File

@ -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(

View File

@ -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"

View File

@ -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(

View File

@ -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

View File

@ -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 (

View File

@ -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&apos;t close app.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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}

View File

@ -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
View 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&apos;t close app.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -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

View File

@ -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' ? (

View File

@ -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;
}); });
}); });

View File

@ -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}

View File

@ -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>
); );

View File

@ -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
)} )}
> >

View File

@ -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>

View File

@ -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(

View File

@ -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

View File

@ -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" />

View File

@ -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&apos;ve no new notifications Yo!, you&apos;ve no new notifications
</p> </p>
</div> </div>

View File

@ -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>

View File

@ -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

View File

@ -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

View 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 };
}

View File

@ -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
} }