This commit is contained in:
Ren Amamiya
2023-09-24 07:55:27 +07:00
parent c9bc7b81dd
commit 50f90ddcc2
12 changed files with 510 additions and 351 deletions

View File

@@ -7,6 +7,8 @@ import { OnboardingScreen } from '@app/auth/onboarding';
import { BrowseScreen } from '@app/browse';
import { ErrorScreen } from '@app/error';
import { useStorage } from '@libs/storage/provider';
import { Frame } from '@shared/frame';
import { LoaderIcon } from '@shared/icons';
import { AppLayout } from '@shared/layouts/app';
@@ -14,275 +16,279 @@ import { AuthLayout } from '@shared/layouts/auth';
import { NoteLayout } from '@shared/layouts/note';
import { SettingsLayout } from '@shared/layouts/settings';
import { checkActiveAccount } from '@utils/checkActiveAccount';
import './index.css';
async function Loader() {
try {
const account = await checkActiveAccount();
const stronghold = sessionStorage.getItem('stronghold');
const privkey = JSON.parse(stronghold).state.privkey || null;
const onboarding = localStorage.getItem('onboarding');
const step = JSON.parse(onboarding).state.step || null;
if (!account) {
return redirect('/auth/welcome');
} else {
if (step) {
return redirect(step);
}
if (!privkey) {
return redirect('/auth/unlock');
}
}
return null;
} catch (e) {
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
}
}
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
errorElement: <ErrorScreen />,
loader: Loader,
children: [
{
path: '',
async lazy() {
const { SpaceScreen } = await import('@app/space');
return { Component: SpaceScreen };
},
},
{
path: 'browse',
element: <BrowseScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { BrowseUsersScreen } = await import('@app/browse/users');
return { Component: BrowseUsersScreen };
},
},
{
path: 'relays',
async lazy() {
const { BrowseRelaysScreen } = await import('@app/browse/relays');
return { Component: BrowseRelaysScreen };
},
},
],
},
{
path: 'users/:pubkey',
async lazy() {
const { UserScreen } = await import('@app/users');
return { Component: UserScreen };
},
},
{
path: 'chats/:pubkey',
async lazy() {
const { ChatScreen } = await import('@app/chats');
return { Component: ChatScreen };
},
},
{
path: 'notifications',
async lazy() {
const { NotificationScreen } = await import('@app/notifications');
return { Component: NotificationScreen };
},
},
{
path: 'nwc',
async lazy() {
const { NWCScreen } = await import('@app/nwc');
return { Component: NWCScreen };
},
},
],
},
{
path: '/notes',
element: <NoteLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'text/:id',
async lazy() {
const { TextNoteScreen } = await import('@app/notes/text');
return { Component: TextNoteScreen };
},
},
{
path: 'article/:id',
async lazy() {
const { ArticleNoteScreen } = await import('@app/notes/article');
return { Component: ArticleNoteScreen };
},
},
],
},
{
path: '/splashscreen',
errorElement: <ErrorScreen />,
async lazy() {
const { SplashScreen } = await import('@app/splash');
return { Component: SplashScreen };
},
},
{
path: '/auth',
element: <AuthLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'welcome',
async lazy() {
const { WelcomeScreen } = await import('@app/auth/welcome');
return { Component: WelcomeScreen };
},
},
{
path: 'import',
element: <AuthImportScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { ImportStep1Screen } = await import('@app/auth/import/step-1');
return { Component: ImportStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { ImportStep2Screen } = await import('@app/auth/import/step-2');
return { Component: ImportStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { ImportStep3Screen } = await import('@app/auth/import/step-3');
return { Component: ImportStep3Screen };
},
},
],
},
{
path: 'create',
element: <AuthCreateScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { CreateStep1Screen } = await import('@app/auth/create/step-1');
return { Component: CreateStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { CreateStep2Screen } = await import('@app/auth/create/step-2');
return { Component: CreateStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { CreateStep3Screen } = await import('@app/auth/create/step-3');
return { Component: CreateStep3Screen };
},
},
],
},
{
path: 'onboarding',
element: <OnboardingScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { OnboardStep1Screen } = await import('@app/auth/onboarding/step-1');
return { Component: OnboardStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { OnboardStep2Screen } = await import('@app/auth/onboarding/step-2');
return { Component: OnboardStep2Screen };
},
},
],
},
{
path: 'complete',
async lazy() {
const { CompleteScreen } = await import('@app/auth/complete');
return { Component: CompleteScreen };
},
},
{
path: 'unlock',
async lazy() {
const { UnlockScreen } = await import('@app/auth/unlock');
return { Component: UnlockScreen };
},
},
{
path: 'migrate',
async lazy() {
const { MigrateScreen } = await import('@app/auth/migrate');
return { Component: MigrateScreen };
},
},
{
path: 'reset',
async lazy() {
const { ResetScreen } = await import('@app/auth/reset');
return { Component: ResetScreen };
},
},
],
},
{
path: '/settings',
element: <SettingsLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { GeneralSettingsScreen } = await import('@app/settings/general');
return { Component: GeneralSettingsScreen };
},
},
{
path: 'backup',
async lazy() {
const { AccountSettingsScreen } = await import('@app/settings/account');
return { Component: AccountSettingsScreen };
},
},
],
},
]);
export default function App() {
const { db } = useStorage();
const accountLoader = async () => {
try {
const account = await db.checkAccount();
const stronghold = sessionStorage.getItem('stronghold');
const privkey = JSON.parse(stronghold).state.privkey || null;
const onboarding = localStorage.getItem('onboarding');
const step = JSON.parse(onboarding).state.step || null;
if (!account) {
return redirect('/auth/welcome');
} else {
if (step) {
return redirect(step);
}
if (!privkey) {
return redirect('/auth/unlock');
}
}
return null;
} catch (e) {
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
}
};
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
errorElement: <ErrorScreen />,
loader: accountLoader,
children: [
{
path: '',
async lazy() {
const { SpaceScreen } = await import('@app/space');
return { Component: SpaceScreen };
},
},
{
path: 'browse',
element: <BrowseScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { BrowseUsersScreen } = await import('@app/browse/users');
return { Component: BrowseUsersScreen };
},
},
{
path: 'relays',
async lazy() {
const { BrowseRelaysScreen } = await import('@app/browse/relays');
return { Component: BrowseRelaysScreen };
},
},
],
},
{
path: 'users/:pubkey',
async lazy() {
const { UserScreen } = await import('@app/users');
return { Component: UserScreen };
},
},
{
path: 'chats/:pubkey',
async lazy() {
const { ChatScreen } = await import('@app/chats');
return { Component: ChatScreen };
},
},
{
path: 'notifications',
async lazy() {
const { NotificationScreen } = await import('@app/notifications');
return { Component: NotificationScreen };
},
},
{
path: 'nwc',
async lazy() {
const { NWCScreen } = await import('@app/nwc');
return { Component: NWCScreen };
},
},
],
},
{
path: '/notes',
element: <NoteLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'text/:id',
async lazy() {
const { TextNoteScreen } = await import('@app/notes/text');
return { Component: TextNoteScreen };
},
},
{
path: 'article/:id',
async lazy() {
const { ArticleNoteScreen } = await import('@app/notes/article');
return { Component: ArticleNoteScreen };
},
},
],
},
{
path: '/splashscreen',
errorElement: <ErrorScreen />,
async lazy() {
const { SplashScreen } = await import('@app/splash');
return { Component: SplashScreen };
},
},
{
path: '/auth',
element: <AuthLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'welcome',
async lazy() {
const { WelcomeScreen } = await import('@app/auth/welcome');
return { Component: WelcomeScreen };
},
},
{
path: 'import',
element: <AuthImportScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { ImportStep1Screen } = await import('@app/auth/import/step-1');
return { Component: ImportStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { ImportStep2Screen } = await import('@app/auth/import/step-2');
return { Component: ImportStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { ImportStep3Screen } = await import('@app/auth/import/step-3');
return { Component: ImportStep3Screen };
},
},
],
},
{
path: 'create',
element: <AuthCreateScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { CreateStep1Screen } = await import('@app/auth/create/step-1');
return { Component: CreateStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { CreateStep2Screen } = await import('@app/auth/create/step-2');
return { Component: CreateStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { CreateStep3Screen } = await import('@app/auth/create/step-3');
return { Component: CreateStep3Screen };
},
},
],
},
{
path: 'onboarding',
element: <OnboardingScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { OnboardStep1Screen } = await import(
'@app/auth/onboarding/step-1'
);
return { Component: OnboardStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { OnboardStep2Screen } = await import(
'@app/auth/onboarding/step-2'
);
return { Component: OnboardStep2Screen };
},
},
],
},
{
path: 'complete',
async lazy() {
const { CompleteScreen } = await import('@app/auth/complete');
return { Component: CompleteScreen };
},
},
{
path: 'unlock',
async lazy() {
const { UnlockScreen } = await import('@app/auth/unlock');
return { Component: UnlockScreen };
},
},
{
path: 'migrate',
async lazy() {
const { MigrateScreen } = await import('@app/auth/migrate');
return { Component: MigrateScreen };
},
},
{
path: 'reset',
async lazy() {
const { ResetScreen } = await import('@app/auth/reset');
return { Component: ResetScreen };
},
},
],
},
{
path: '/settings',
element: <SettingsLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { GeneralSettingsScreen } = await import('@app/settings/general');
return { Component: GeneralSettingsScreen };
},
},
{
path: 'backup',
async lazy() {
const { AccountSettingsScreen } = await import('@app/settings/account');
return { Component: AccountSettingsScreen };
},
},
],
},
]);
return (
<RouterProvider
router={router}
@@ -291,6 +297,7 @@ export default function App() {
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
</Frame>
}
future={{ v7_startTransition: true }}
/>
);
}

View File

@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { PlusIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { NIP05 } from '@shared/nip05';
import { TextNote } from '@shared/notes';
@@ -50,14 +51,26 @@ export const UserDrawer = memo(function UserDrawer({ pubkey }: { pubkey: string
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button type="button">
<User pubkey={pubkey} variant="avatar" />
</button>
</Dialog.Trigger>
<div className="group relative">
<Dialog.Trigger asChild>
<button type="button" className="relative z-10">
<User pubkey={pubkey} variant="avatar" />
</button>
</Dialog.Trigger>
<div className="absolute -bottom-14 left-0 flex flex-col opacity-0 transition-all duration-300 ease-smooth group-hover:-bottom-16 group-hover:opacity-100">
<div className="mt-4">
<button
type="button"
className="inline-flex h-12 w-12 items-center justify-center rounded-lg bg-white/10 backdrop-blur-xl hover:bg-white/20"
>
<PlusIcon className="h-4 w-4 text-white" />
</button>
</div>
</div>
</div>
<Dialog.Portal>
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] items-center justify-center">
<div className="h-full w-full overflow-y-auto border-l border-white/10 bg-white/20 px-3 py-3 backdrop-blur-xl">
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] items-center justify-center px-4 pb-4 pt-16">
<div className="h-full w-full overflow-y-auto rounded-lg border-t border-white/10 bg-white/20 px-3 py-3 backdrop-blur-xl">
{status === 'loading' ? (
<div>
<p>Loading...</p>

View File

@@ -1,12 +1,14 @@
import { NavLink, Outlet } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { DotsPattern } from '@shared/icons';
export function BrowseScreen() {
return (
<div className="relative h-full w-full">
<div className="absolute left-0 right-0 top-4 flex w-full items-center justify-between px-3">
<div className="absolute left-0 right-0 top-4 z-30 flex w-full items-center justify-between px-3">
<div className="w-10" />
<div className="inline-flex gap-1 rounded-full border-t border-white/10 bg-white/20 p-1">
<div className="inline-flex gap-1 rounded-full border-t border-white/10 bg-white/20 p-1 backdrop-blur-xl">
<NavLink
to="/browse/"
className={({ isActive }) =>
@@ -32,7 +34,12 @@ export function BrowseScreen() {
</div>
<div className="w-10" />
</div>
<Outlet />
<div className="absolute z-10 h-full w-full">
<DotsPattern className="h-full w-full text-white/10" />
</div>
<div className="relative z-20">
<Outlet />
</div>
</div>
);
}

View File

@@ -1,21 +1,24 @@
import { useMemo } from 'react';
import { UserDrawer } from '@app/browse/components/userDrawer';
import { useStorage } from '@libs/storage/provider';
import { User } from '@shared/user';
import { getMultipleRandom } from '@utils/transform';
export function BrowseUsersScreen() {
const { db } = useStorage();
const data = useMemo(() => getMultipleRandom(db.account.follows, 10), []);
return (
<div className="flex h-full w-full items-center justify-center">
<div>
<User pubkey={db.account.pubkey} variant="avatar" />
<div className="mt-4 flex flex-col gap-2">
{db.account.follows.map((user) => (
<UserDrawer key={user} pubkey={user} />
))}
</div>
<div>
<User pubkey={db.account.pubkey} variant="avatar" />
<div className="flex items-center gap-4">
{data.map((user) => (
<UserDrawer key={user} pubkey={user} />
))}
</div>
</div>
);

View File

@@ -1,6 +1,5 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { BaseDirectory, removeFile } from '@tauri-apps/api/fs';
import { Platform } from '@tauri-apps/api/os';
import Database from 'tauri-plugin-sql-api';
import { Stronghold } from 'tauri-plugin-stronghold-api';
@@ -11,13 +10,11 @@ import { Account, DBEvent, Relays, Widget } from '@utils/types';
export class LumeStorage {
public db: Database;
public platform: Platform;
public secureDB: Stronghold;
public account: Account | null = null;
constructor(sqlite: Database, platform: Platform, stronghold?: Stronghold) {
constructor(sqlite: Database, stronghold?: Stronghold) {
this.db = sqlite;
this.platform = platform ?? undefined;
this.secureDB = stronghold ?? undefined;
this.account = null;
}
@@ -59,6 +56,13 @@ export class LumeStorage {
return await removeFile('lume.stronghold', { dir: BaseDirectory.AppConfig });
}
public async checkAccount() {
const result: Array<Account> = await this.db.select(
'SELECT * FROM accounts WHERE is_active = 1;'
);
return result.length > 0;
}
public async getActiveAccount() {
const results: Array<Account> = await this.db.select(
'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'

View File

@@ -1,6 +1,4 @@
import { message } from '@tauri-apps/api/dialog';
import { platform } from '@tauri-apps/api/os';
import { appConfigDir } from '@tauri-apps/api/path';
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
@@ -17,14 +15,10 @@ const StorageContext = createContext<StorageContext>({
const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const [db, setDB] = useState<LumeStorage>(undefined);
async function initLumeStorage() {
const initLumeStorage = async () => {
try {
const dir = await appConfigDir();
const sqlite = await Database.load('sqlite:lume.db');
const platformName = await platform();
const lumeStorage = new LumeStorage(sqlite, platformName);
console.log('App config dir: ', dir);
const lumeStorage = new LumeStorage(sqlite);
if (!lumeStorage.account) await lumeStorage.getActiveAccount();
setDB(lumeStorage);
@@ -34,7 +28,7 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
type: 'error',
});
}
}
};
useEffect(() => {
if (!db) initLumeStorage();

20
src/shared/icons/dots.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { SVGProps } from 'react';
export function DotsPattern(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg {...props}>
<pattern
id="pattern-circles"
width="30"
height="30"
x="0"
y="0"
patternContentUnits="userSpaceOnUse"
patternUnits="userSpaceOnUse"
>
<circle cx="2" cy="2" r="1.626" fill="currentColor"></circle>
</pattern>
<rect width="100%" height="100%" x="0" y="0" fill="url(#pattern-circles)"></rect>
</svg>
);
}

View File

@@ -65,3 +65,4 @@ export * from './alby';
export * from './stars';
export * from './nwc';
export * from './timeline';
export * from './dots';

View File

@@ -1,28 +0,0 @@
import Database from 'tauri-plugin-sql-api';
import { Account } from '@utils/types';
async function connect(): Promise<Database> {
let db: null | Database = null;
if (db) {
return db;
}
try {
db = await Database.load('sqlite:lume.db');
} catch (e) {
throw new Error('Failed to connect to database, error: ', e);
}
return db;
}
export async function checkActiveAccount() {
const db = await connect();
const result: Array<Account> = await db.select(
'SELECT * FROM accounts WHERE is_active = 1;'
);
return result.length > 0;
}

View File

@@ -24,3 +24,9 @@ export function getRepostID(tags: NDKTag[]) {
return quoteID;
}
// get random n elements from array
export function getMultipleRandom(arr: string[], num: number) {
const shuffled = [...arr].sort(() => 0.5 - Math.random());
return shuffled.slice(0, num);
}