migrated to vite and vite-plugin-ssr

This commit is contained in:
Ren Amamiya 2023-04-22 17:56:09 +07:00
parent c50e507c14
commit b9bafc851e
67 changed files with 911 additions and 1060 deletions

View File

@ -15,6 +15,7 @@
"^@stores/(.*)$",
"^@utils/(.*)$",
"^@assets/(.*)$",
"^@renderer/(.*)$",
"<THIRD_PARTY_MODULES>",
"^[./]"
],

View File

@ -2,8 +2,8 @@
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devPath": "http://localhost:1420",
"distDir": "../out",
"devPath": "http://localhost:3000",
"distDir": "../dist",
"withGlobalTauri": true
},
"package": {

View File

@ -1,13 +0,0 @@
import '@assets/global.css';
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<body className="cursor-default select-none overflow-hidden font-sans antialiased">
<Providers>{children}</Providers>
</body>
</html>
);
}

View File

@ -1,7 +0,0 @@
export default function Page() {
return (
<div className="flex h-full w-full items-center justify-center">
<p className="text-sm text-zinc-400">Sorry, this feature under development, it will come in the next version</p>
</div>
);
}

View File

@ -1,10 +0,0 @@
'use client';
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
const id = searchParams.get('event-id');
return <div className="scrollbar-hide flex h-full flex-col gap-2 overflow-y-auto py-3">{id}</div>;
}

View File

@ -1,51 +0,0 @@
'use client';
import ProfileFollowers from '@components/profile/followers';
import ProfileFollows from '@components/profile/follows';
import ProfileMetadata from '@components/profile/metadata';
import ProfileNotes from '@components/profile/notes';
import * as Tabs from '@radix-ui/react-tabs';
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
const pubkey = searchParams.get('pubkey');
return (
<div className="scrollbar-hide h-full w-full overflow-y-auto">
<ProfileMetadata id={pubkey} />
<Tabs.Root className="flex w-full flex-col" defaultValue="notes">
<Tabs.List className="flex border-b border-zinc-800">
<Tabs.Trigger
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
value="notes"
>
Notes
</Tabs.Trigger>
<Tabs.Trigger
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium text-zinc-400 outline-none placeholder:leading-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
value="followers"
>
Followers
</Tabs.Trigger>
<Tabs.Trigger
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
value="following"
>
Following
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="notes">
<ProfileNotes id={pubkey} />
</Tabs.Content>
<Tabs.Content value="followers">
<ProfileFollowers id={pubkey} />
</Tabs.Content>
<Tabs.Content value="following">
<ProfileFollows id={pubkey} />
</Tabs.Content>
</Tabs.Root>
</div>
);
}

View File

@ -1,169 +0,0 @@
'use client';
import { RelayContext } from '@components/relaysProvider';
import { createAccount } from '@utils/storage';
import { EyeClose, EyeEmpty } from 'iconoir-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
import { useCallback, useContext, useMemo, useState } from 'react';
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
const config: Config = {
dictionaries: [names],
};
export default function Page() {
const router = useRouter();
const [pool, relays]: any = useContext(RelayContext);
const [type, setType] = useState('password');
const [loading, setLoading] = useState(false);
const privkey = useMemo(() => generatePrivateKey(), []);
const name = useMemo(() => uniqueNamesGenerator(config).toString(), []);
const pubkey = getPublicKey(privkey);
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(privkey);
// auto-generated profile metadata
const metadata: any = useMemo(
() => ({
display_name: name,
name: name,
username: name.toLowerCase(),
picture: 'https://void.cat/d/KmypFh2fBdYCEvyJrPiN89.webp',
}),
[name]
);
// toggle privatek key
const showPrivateKey = () => {
if (type === 'password') {
setType('text');
} else {
setType('password');
}
};
// create account and broadcast to all relays
const submit = useCallback(async () => {
setLoading(true);
// build event
const event: any = {
content: JSON.stringify(metadata),
created_at: Math.floor(Date.now() / 1000),
kind: 0,
pubkey: pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, privkey);
// insert to database
createAccount(pubkey, privkey, metadata);
// broadcast
pool.publish(event, relays);
// redirect to next step
router.push(`/onboarding/create/step-2?pubkey=${pubkey}&privkey=${privkey}`, { forceOptimisticNavigation: true });
}, [pool, pubkey, privkey, metadata, relays, router]);
return (
<div className="grid h-full w-full grid-rows-5">
<div className="row-span-1 mx-auto flex w-full max-w-md items-center justify-center">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Create new account
</h1>
</div>
<div className="row-span-4">
<div className="mx-auto w-full max-w-md">
<div className="mb-8 flex flex-col gap-4">
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">Public Key</label>
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<input
readOnly
value={npub}
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
/>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">Private Key</label>
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<input
readOnly
type={type}
value={nsec}
className="relative w-full rounded-lg border border-black/5 py-2.5 pl-3.5 pr-11 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
/>
<button
onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{type === 'password' ? (
<EyeClose width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
) : (
<EyeEmpty width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
)}
</button>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">Default Profile (you can change it later)</label>
<div className="relative w-full shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<div className="relative w-full rounded-lg border border-black/5 px-3.5 py-4 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600">
<div className="flex space-x-2">
<div className="relative h-11 w-11 rounded-md">
<Image className="inline-block rounded-md" src={metadata.picture} alt="" fill={true} />
</div>
<div className="flex-1 space-y-2 py-1">
<div className="flex items-center gap-1">
<p className="font-semibold">{metadata.display_name}</p>
<p className="text-zinc-400">@{metadata.username}</p>
</div>
<div className="space-y-1">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
</div>
<div className="h-2 rounded bg-zinc-700"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="flex h-10 items-center justify-center">
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<button
onClick={() => submit()}
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Continue </span>
</button>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -1,120 +0,0 @@
'use client';
import { CableTag } from 'iconoir-react';
import { useRouter } from 'next/navigation';
import { getPublicKey, nip19 } from 'nostr-tools';
import { Resolver, useForm } from 'react-hook-form';
type FormValues = {
key: string;
};
const resolver: Resolver<FormValues> = async (values) => {
return {
values: values.key ? values : {},
errors: !values.key
? {
key: {
type: 'required',
message: 'This is required.',
},
}
: {},
};
};
export default function Page() {
const router = useRouter();
const {
register,
setError,
handleSubmit,
formState: { errors, isDirty, isValid, isSubmitting },
} = useForm<FormValues>({ resolver });
const onSubmit = async (data: any) => {
try {
let privkey = data['key'];
if (privkey.substring(0, 4) === 'nsec') {
privkey = nip19.decode(privkey).data;
}
if (typeof getPublicKey(privkey) === 'string') {
router.push(`/onboarding/login/step-2?privkey=${privkey}`, { forceOptimisticNavigation: true });
}
} catch (error) {
setError('key', {
type: 'custom',
message: 'Private Key is invalid, please check again',
});
}
};
return (
<div className="grid h-full w-full grid-rows-5">
<div className="row-span-1 mx-auto flex w-full max-w-md items-center justify-center">
<h1 className="bg-gradient-to-br from-zinc-200 via-white to-zinc-300 bg-clip-text text-3xl font-semibold text-transparent">
Login with Private Key
</h1>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="row-span-4">
<div className="mx-auto w-full max-w-md">
<div className="flex flex-col gap-4">
<div>
{/* #TODO: add function */}
<button className="inline-flex w-full transform items-center justify-center gap-1.5 rounded-lg bg-zinc-700 px-3.5 py-2.5 font-medium text-zinc-200 shadow-input ring-1 ring-zinc-600 active:translate-y-1">
{/* #TODO: change to nostr connect logo */}
<CableTag width={20} height={20} className="text-fuchsia-500" />
<span>Continue with Nostr Connect</span>
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-zinc-800"></div>
</div>
<div className="relative flex justify-center">
<span className="bg-black px-2 text-sm text-zinc-500">or</span>
</div>
</div>
<div className="flex flex-col gap-0.5">
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<input
{...register('key', { required: true, minLength: 32 })}
type={'password'}
placeholder="Paste private key here..."
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 text-center shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<span className="text-xs text-red-400">{errors.key && <p>{errors.key.message}</p>}</span>
</div>
</div>
<div className="mt-3 flex h-10 items-center justify-center">
{isSubmitting ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<button
type="submit"
disabled={!isDirty || !isValid}
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Continue </span>
</button>
)}
</div>
</div>
</form>
</div>
);
}

View File

@ -1,145 +0,0 @@
'use client';
import { RelayContext } from '@components/relaysProvider';
import { DEFAULT_AVATAR } from '@stores/constants';
import { fetchProfileMetadata } from '@utils/hooks/useProfileMetadata';
import { shortenKey } from '@utils/shortenKey';
import { createAccount, createPleb, updateAccount } from '@utils/storage';
import { nip02ToArray } from '@utils/transform';
import Image from 'next/image';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { getPublicKey } from 'nostr-tools';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
export default function Page() {
const searchParams = useSearchParams();
const privkey = searchParams.get('privkey');
const [pool, relays]: any = useContext(RelayContext);
const [profile, setProfile] = useState({ metadata: null });
const [done, setDone] = useState(false);
const timeout = useRef(null);
const pubkey = getPublicKey(privkey);
const createPlebs = useCallback(async (tags: string[]) => {
for (const tag of tags) {
fetchProfileMetadata(tag[1])
.then((res: any) => createPleb(tag[1], res.content))
.catch(console.error);
}
}, []);
useEffect(() => {
const unsubscribe = pool.subscribe(
[
{
kinds: [0, 3],
authors: [pubkey],
},
],
relays,
(event: any) => {
if (event.kind === 0) {
// create account
createAccount(pubkey, privkey, event.content);
// update state
setProfile({
metadata: JSON.parse(event.content),
});
} else {
if (event.tags.length > 0) {
createPlebs(event.tags);
const arr = nip02ToArray(event.tags);
// update account's folllows with NIP-02 tag list
updateAccount('follows', arr, pubkey);
}
}
},
undefined,
() => {
timeout.current = setTimeout(() => setDone(true), 5000);
},
{
unsubscribeOnEose: true,
logAllEvents: false,
}
);
return () => {
unsubscribe();
clearTimeout(timeout.current);
};
}, [pool, relays, pubkey, privkey, createPlebs]);
return (
<div className="grid h-full w-full grid-rows-5">
<div className="row-span-1 flex items-center justify-center">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Bringing back your profile...
</h1>
</div>
<div className="row-span-4 flex flex-col gap-8">
<div className="mx-auto w-full max-w-md">
<div className="mb-4 flex flex-col gap-2">
<div className="w-full rounded-lg bg-zinc-900 p-4 shadow-input ring-1 ring-zinc-800">
<div className="flex space-x-4">
<div className="relative h-10 w-10 rounded-full">
<Image
className="inline-block rounded-full"
src={profile.metadata?.picture || DEFAULT_AVATAR}
alt=""
fill={true}
/>
</div>
<div className="flex-1 space-y-4 py-1">
<div className="flex items-center gap-2">
<p className="font-semibold">{profile.metadata?.display_name || profile.metadata?.name}</p>
<span className="leading-tight text-zinc-500">·</span>
<p className="text-zinc-500">@{profile.metadata?.username || (pubkey && shortenKey(pubkey))}</p>
</div>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
</div>
<div className="h-2 rounded bg-zinc-700"></div>
</div>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-center">
{done === false ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<Link
prefetch={false}
href="/"
className="inline-flex w-full transform items-center justify-center rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Done! Go to newsfeed</span>
</Link>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -1,9 +0,0 @@
'use client';
import dynamic from 'next/dynamic';
const RelayProvider = dynamic(() => import('@components/relaysProvider'), { ssr: false });
export function Providers({ children }: { children: React.ReactNode }) {
return <RelayProvider>{children}</RelayProvider>;
}

View File

@ -1,25 +0,0 @@
'use client';
import Link from 'next/link';
import { useSelectedLayoutSegments } from 'next/navigation';
export const ActiveLink = ({
href,
className,
activeClassName,
children,
}: {
href: string;
className: string;
activeClassName: string;
children: React.ReactNode;
}) => {
const segments = useSelectedLayoutSegments();
const isActive = href.includes(segments[1]);
return (
<Link prefetch={false} href={href} className={`${className}` + ' ' + (isActive ? `${activeClassName}` : '')}>
{children}
</Link>
);
};

View File

@ -1,24 +1,20 @@
'use client';
import { platform } from '@tauri-apps/api/os';
import { ArrowLeft, ArrowRight, Refresh } from 'iconoir-react';
import { useRouter } from 'next/navigation';
import { useLayoutEffect, useState } from 'react';
export default function AppActions() {
const router = useRouter();
const [os, setOS] = useState('');
const goBack = () => {
router.back();
window.history.back();
};
const goForward = () => {
router.forward();
window.history.forward();
};
const reload = () => {
router.refresh();
window.location.reload();
};
useLayoutEffect(() => {

View File

@ -1,12 +1,5 @@
import dynamic from 'next/dynamic';
const AppActions = dynamic(() => import('@components/appHeader/actions'), {
ssr: false,
});
const EventCollector = dynamic(() => import('@components/eventCollector'), {
ssr: false,
});
import AppActions from '@components/appHeader/actions';
import EventCollector from '@components/eventCollector';
export default function AppHeader({ collector }: { collector: boolean }) {
return (

View File

@ -10,19 +10,6 @@ export default function ChannelList() {
return (
<div className="flex flex-col gap-px">
{/*
<Link
href="/explore/channels"
className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
>
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800">
<Globe width={12} height={12} className="text-zinc-500" />
</div>
<div>
<h5 className="text-sm font-medium text-zinc-500 group-hover:text-zinc-400">Browse channels</h5>
</div>
</Link>
*/}
{list.map((item) => (
<ChannelListItem key={item.event_id} data={item} />
))}

View File

@ -1,31 +1,21 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useChannelMetadata } from '@utils/hooks/useChannelMetadata';
import Link from 'next/link';
export const ChannelListItem = ({ data }: { data: any }) => {
const channel = useChannelMetadata(data.event_id, data.metadata);
return (
<Link
prefetch={false}
href={`/nostr/channel?channel-id=${data.event_id}`}
<a
href={`channel?id=${data.event_id}`}
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
>
<div className="relative h-5 w-5 shrink-0 overflow-hidden rounded">
<ImageWithFallback
src={channel?.picture || DEFAULT_AVATAR}
alt={data.event_id}
fill={true}
className="rounded object-cover"
/>
<div className="relative h-5 w-5 shrink-0 rounded">
<img src={channel?.picture || DEFAULT_AVATAR} alt={data.event_id} className="h-5 w-5 rounded object-cover" />
</div>
<div>
<h5 className="truncate text-sm font-medium text-zinc-400">{channel?.name.toLowerCase()}</h5>
</div>
</Link>
</a>
);
};

View File

@ -1,13 +1,11 @@
import { ChatListItem } from '@components/chats/chatListItem';
import { ChatModal } from '@components/chats/chatModal';
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { getChats } from '@utils/storage';
import useLocalStorage from '@rehooks/local-storage';
import Link from 'next/link';
import { useEffect, useState } from 'react';
export default function ChatList() {
@ -33,17 +31,15 @@ export default function ChatList() {
return (
<div className="flex flex-col gap-px">
<Link
prefetch={false}
href={`/nostr/chat?pubkey=${activeAccount.pubkey}`}
<a
href={`/chat?pubkey=${activeAccount.pubkey}`}
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
>
<div className="relative h-5 w-5 shrink overflow-hidden rounded bg-white">
<ImageWithFallback
<div className="relative h-5 w-5 shrink rounded bg-white">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={activeAccount.pubkey}
fill={true}
className="rounded object-cover"
className="h-5 w-5 rounded object-cover"
/>
</div>
<div>
@ -51,7 +47,7 @@ export default function ChatList() {
{profile?.display_name || profile?.name} <span className="text-zinc-500">(you)</span>
</h5>
</div>
</Link>
</a>
{list.map((item) => (
<ChatListItem key={item.id} pubkey={item.pubkey} />
))}

View File

@ -1,34 +1,24 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
import { shortenKey } from '@utils/shortenKey';
import Link from 'next/link';
export const ChatListItem = ({ pubkey }: { pubkey: string }) => {
const profile = useProfileMetadata(pubkey);
return (
<Link
prefetch={false}
href={`/nostr/chat?pubkey=${pubkey}`}
<a
href={`/chat?pubkey=${pubkey}`}
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
>
<div className="relative h-5 w-5 shrink overflow-hidden rounded">
<ImageWithFallback
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
fill={true}
className="rounded object-cover"
/>
<div className="relative h-5 w-5 shrink rounded">
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-5 w-5 rounded object-cover" />
</div>
<div>
<h5 className="text-sm font-medium text-zinc-400">
{profile?.display_name || profile?.name || shortenKey(pubkey)}
</h5>
</div>
</Link>
</a>
);
};

View File

@ -1,28 +1,24 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { shortenKey } from '@utils/shortenKey';
import { useRouter } from 'next/navigation';
import { navigate } from 'vite-plugin-ssr/client/router';
export const ChatModalUser = ({ data }: { data: any }) => {
const router = useRouter();
const profile = JSON.parse(data.metadata);
const openNewChat = () => {
router.push(`/nostr/chat?pubkey=${data.pubkey}`, { forceOptimisticNavigation: true });
navigate(`/chat?pubkey=${data.pubkey}`);
};
return (
<div className="group flex items-center justify-between px-3 py-2 hover:bg-zinc-800">
<div className="flex items-center gap-2">
<div className="relative h-10 w-10 shrink overflow-hidden rounded-md">
<ImageWithFallback
<div className="relative h-10 w-10 shrink-0 rounded-md">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={data.pubkey}
fill={true}
className="rounded-md object-cover"
className="h-10 w-10 rounded-md object-cover"
/>
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">

View File

@ -1,5 +1,3 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
@ -15,13 +13,8 @@ export const MessageUser = ({ pubkey, time }: { pubkey: string; time: number })
return (
<div className="group flex items-start gap-3">
<div className="relative h-9 w-9 shrink overflow-hidden rounded-md">
<ImageWithFallback
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
fill={true}
className="rounded-md object-cover"
/>
<div className="relative h-9 w-9 shrink rounded-md">
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-9 w-9 rounded-md object-cover" />
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex items-baseline gap-2 text-sm">

View File

@ -1,5 +1,3 @@
'use client';
import { NetworkStatusIndicator } from '@components/networkStatusIndicator';
import { RelayContext } from '@components/relaysProvider';

View File

@ -1,36 +0,0 @@
import { noteContentAtom } from '@stores/note';
import data from '@emoji-mart/data';
import Picker from '@emoji-mart/react';
import * as Popover from '@radix-ui/react-popover';
import { Emoji } from 'iconoir-react';
import { useAtom } from 'jotai';
export default function EmojiPicker() {
const [value, setValue] = useAtom(noteContentAtom);
return (
<Popover.Root>
<Popover.Trigger asChild>
<button className="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-zinc-700">
<Emoji width={16} height={16} className="text-zinc-400" />
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
className="rounded-md will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=top]:animate-slideDownAndFade"
sideOffset={5}
>
<Picker
data={data}
emojiSize={16}
navPosition={'none'}
skinTonePosition={'none'}
onEmojiSelect={(res) => setValue(value + ' ' + res.native)}
/>
<Popover.Arrow className="fill-[#141516]" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}

View File

@ -1,26 +0,0 @@
import { DEFAULT_AVATAR } from '@stores/constants';
import Image from 'next/image';
import { memo, useEffect, useState } from 'react';
export const ImageWithFallback = memo(function ImageWithFallback({
src,
alt,
fill,
className,
}: {
src: string;
alt: string;
fill: boolean;
className: string;
}) {
const [error, setError] = useState(null);
useEffect(() => {
setError(null);
}, [src]);
return (
<Image src={error ? DEFAULT_AVATAR : src} alt={alt} fill={fill} className={className} onError={setError} priority />
);
});

View File

@ -2,7 +2,7 @@ import AppHeader from '@components/appHeader';
import MultiAccounts from '@components/multiAccounts';
import Navigation from '@components/navigation';
export default function NostrLayout({ children }: { children: React.ReactNode }) {
export default function NewsfeedLayout({ children }: { children: React.ReactNode }) {
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
<div className="flex h-screen w-full flex-col">

12
src/components/link.tsx Normal file
View File

@ -0,0 +1,12 @@
import { usePageContext } from '@utils/hooks/usePageContext';
import { AnchorHTMLAttributes, ClassAttributes } from 'react';
export function Link(
props: JSX.IntrinsicAttributes & ClassAttributes<HTMLAnchorElement> & AnchorHTMLAttributes<HTMLAnchorElement>,
activeClass: string
) {
const pageContext = usePageContext();
const className = [props.className, pageContext.urlPathname === props.href && activeClass].filter(Boolean).join(' ');
return <a {...props} className={className} />;
}

View File

@ -1,20 +1,16 @@
'use client';
import { DEFAULT_AVATAR } from '@stores/constants';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { writeText } from '@tauri-apps/api/clipboard';
import { LogOut, ProfileCircle, Settings } from 'iconoir-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { nip19 } from 'nostr-tools';
import { navigate } from 'vite-plugin-ssr/client/router';
export const ActiveAccount = ({ user }: { user: any }) => {
const router = useRouter();
const userData = JSON.parse(user.metadata);
const openProfilePage = () => {
router.push(`/nostr/user?pubkey=${user.pubkey}`, { forceOptimisticNavigation: true });
navigate(`/user?pubkey=${user.pubkey}`);
};
const copyPublicKey = async () => {
@ -25,12 +21,10 @@ export const ActiveAccount = ({ user }: { user: any }) => {
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="relative h-11 w-11 rounded-lg">
<Image
<img
src={userData.picture || DEFAULT_AVATAR}
alt="user's avatar"
fill={true}
className="rounded-lg object-cover"
priority
className="h-11 w-11 rounded-lg object-cover"
/>
</button>
</DropdownMenu.Trigger>

View File

@ -1,6 +1,5 @@
import { DEFAULT_AVATAR } from '@stores/constants';
import Image from 'next/image';
import { memo } from 'react';
export const InactiveAccount = memo(function InactiveAccount({ user }: { user: any }) {
@ -12,13 +11,7 @@ export const InactiveAccount = memo(function InactiveAccount({ user }: { user: a
return (
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-lg">
<Image
src={userData.picture || DEFAULT_AVATAR}
alt="user's avatar"
fill={true}
className="rounded-lg object-cover"
priority
/>
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
</button>
);
});

View File

@ -1,5 +1,3 @@
'use client';
import { ActiveAccount } from '@components/multiAccounts/activeAccount';
import { InactiveAccount } from '@components/multiAccounts/inactiveAccount';
@ -11,7 +9,6 @@ import LumeSymbol from '@assets/icons/Lume';
import useLocalStorage from '@rehooks/local-storage';
import { Plus } from 'iconoir-react';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
export default function MultiAccounts() {
@ -38,21 +35,19 @@ export default function MultiAccounts() {
return (
<div className="flex h-full flex-col items-center justify-between px-2 pb-4 pt-3">
<div className="flex flex-col gap-4">
<Link
prefetch={false}
<a
href="/explore"
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg bg-zinc-900 hover:bg-zinc-800"
>
<LumeSymbol className="h-6 w-auto text-zinc-400 group-hover:text-zinc-200" />
</Link>
</a>
<div>{users.map((user) => renderAccount(user))}</div>
<Link
prefetch={false}
<a
href="/onboarding"
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-zinc-600 hover:border-zinc-400"
>
<Plus width={16} height={16} className="text-zinc-400 group-hover:text-zinc-200" />
</Link>
</a>
</div>
<div className="flex flex-col gap-0.5 text-center">
<span className="animate-moveBg from-fuchsia-300 via-orange-100 to-amber-300 text-sm font-black uppercase leading-tight text-zinc-600 hover:bg-gradient-to-r hover:bg-clip-text hover:text-transparent">

View File

@ -1,5 +1,3 @@
'use client';
import ChannelList from '@components/channels/channelList';
import * as Collapsible from '@radix-ui/react-collapsible';

View File

@ -1,5 +1,3 @@
'use client';
import ChatList from '@components/chats/chatList';
import * as Collapsible from '@radix-ui/react-collapsible';

View File

@ -1,7 +1,3 @@
'use client';
import { ActiveLink } from '@components/activeLink';
import * as Collapsible from '@radix-ui/react-collapsible';
import { Bonfire, NavArrowUp, PeopleTag } from 'iconoir-react';
import { useState } from 'react';
@ -23,22 +19,22 @@ export default function Newsfeed() {
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Newsfeed</h3>
</Collapsible.Trigger>
<Collapsible.Content className="flex flex-col text-zinc-400">
<ActiveLink
href="/nostr/newsfeed/following"
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
<a
href="/newsfeed/following"
//activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
>
<PeopleTag width={16} height={16} className="text-zinc-500" />
<span>Following</span>
</ActiveLink>
<ActiveLink
href="/nostr/newsfeed/circle"
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
</a>
<a
href="/newsfeed/circle"
//activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
>
<Bonfire width={16} height={16} className="text-zinc-500" />
<span>Circle</span>
</ActiveLink>
</a>
</Collapsible.Content>
</div>
</Collapsible.Root>

View File

@ -4,11 +4,10 @@ import { UserExtend } from '@components/user/extend';
import { contentParser } from '@utils/parser';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
const router = useRouter();
const content = contentParser(event.content, event.tags);
const parentNote = () => {
@ -22,13 +21,13 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
const openUserPage = (e) => {
e.stopPropagation();
router.push(`/nostr/user?pubkey=${event.pubkey}`, { forceOptimisticNavigation: true });
navigate(`/user?pubkey=${event.pubkey}`);
};
const openThread = (e) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
router.push(`/nostr/newsfeed/note?id=${event.parent_id}`, { forceOptimisticNavigation: true });
navigate(`/newsfeed/note?id=${event.parent_id}`);
} else {
e.stopPropagation();
}

View File

@ -1,4 +1,3 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { RelayContext } from '@components/relaysProvider';
import { UserExtend } from '@components/user/extend';
@ -7,9 +6,9 @@ import { dateToUnix } from '@utils/getDate';
import * as Dialog from '@radix-ui/react-dialog';
import useLocalStorage from '@rehooks/local-storage';
import { ChatLines, OpenNewWindow } from 'iconoir-react';
import { useRouter } from 'next/navigation';
import { getEventHash, signEvent } from 'nostr-tools';
import { useContext, useState } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
export const NoteComment = ({
count,
@ -24,7 +23,6 @@ export const NoteComment = ({
eventTime: number;
eventContent: any;
}) => {
const router = useRouter();
const [pool, relays]: any = useContext(RelayContext);
const [open, setOpen] = useState(false);
@ -34,7 +32,7 @@ export const NoteComment = ({
const profile = activeAccount.metadata ? JSON.parse(activeAccount.metadata) : null;
const openThread = () => {
router.push(`/nostr/newsfeed/note?id=${eventID}`, { forceOptimisticNavigation: true });
navigate(`/newsfeed/note?id=${eventID}`);
};
const submitEvent = () => {
@ -84,12 +82,7 @@ export const NoteComment = ({
<div className="flex gap-2">
<div>
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
<ImageWithFallback
src={profile?.picture}
alt="user's avatar"
fill={true}
className="rounded-md object-cover"
/>
<img src={profile?.picture} alt="user's avatar" className="h-11 w-11 rounded-md object-cover" />
</div>
</div>
<div className="relative h-36 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">

View File

@ -1,18 +1,9 @@
import Image from 'next/image';
import { memo } from 'react';
export const ImagePreview = memo(function ImagePreview({ url, size }: { url: string; size: string }) {
return (
<div className={`relative h-full ${size === 'large' ? 'w-4/5' : 'w-1/2'} mt-2 rounded-lg border border-zinc-800`}>
<Image
src={url}
alt={url}
width="0"
height="0"
sizes="100vw"
className="h-auto w-full rounded-lg object-cover"
priority
/>
<img src={url} alt={url} className="h-auto w-full rounded-lg object-cover" />
</div>
);
});

View File

@ -1,23 +0,0 @@
import Image from 'next/image';
import Link from 'next/link';
export default function LinkCard({ data }: { data: any }) {
return (
<Link
href={data['url']}
target={'_blank'}
className="relative flex flex-col overflow-hidden rounded-lg border border-zinc-700"
>
<div className="relative aspect-video h-auto w-full">
<Image src={data['image']} alt="image preview" fill={true} className="object-cover" />
</div>
<div className="flex flex-col gap-2 p-4">
<div>
<h5 className="font-semibold leading-tight">{data['title']}</h5>
<p className="text-sm text-zinc-300">{data['description']}</p>
</div>
<span className="text-sm text-zinc-500">{data['url']}</span>
</div>
</Link>
);
}

View File

@ -4,11 +4,10 @@ import { UserExtend } from '@components/user/extend';
import { contentParser } from '@utils/parser';
import { useRouter } from 'next/navigation';
import { memo, useCallback, useContext, useEffect, useState } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
export const RootNote = memo(function RootNote({ event }: { event: any }) {
const router = useRouter();
const [pool, relays]: any = useContext(RelayContext);
const [data, setData] = useState(null);
@ -16,13 +15,13 @@ export const RootNote = memo(function RootNote({ event }: { event: any }) {
const openUserPage = (e) => {
e.stopPropagation();
router.push(`/nostr/user?pubkey=${event.pubkey}`, { forceOptimisticNavigation: true });
navigate(`/user?pubkey=${event.pubkey}`);
};
const openThread = (e) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
router.push(`/nostr/newsfeed/note?id=${event.parent_id}`, { forceOptimisticNavigation: true });
navigate(`/newsfeed/note?id=${event.parent_id}`);
} else {
e.stopPropagation();
}

View File

@ -1,5 +1,3 @@
'use client';
import { RelayContext } from '@components/relaysProvider';
import { UserFollow } from '@components/user/follow';

View File

@ -1,5 +1,3 @@
'use client';
import { RelayContext } from '@components/relaysProvider';
import { UserFollow } from '@components/user/follow';

View File

@ -1,6 +1,3 @@
'use client';
import { ImageWithFallback } from '@components/imageWithFallback';
import { RelayContext } from '@components/relaysProvider';
import { DEFAULT_AVATAR } from '@stores/constants';
@ -8,7 +5,6 @@ import { DEFAULT_AVATAR } from '@stores/constants';
import { shortenKey } from '@utils/shortenKey';
import destr from 'destr';
import Image from 'next/image';
import { Author } from 'nostr-relaypool';
import { useContext, useEffect, useState } from 'react';
@ -27,21 +23,11 @@ export default function ProfileMetadata({ id }: { id: string }) {
<>
<div className="relative">
<div className="relative h-56 w-full rounded-t-lg bg-zinc-800">
<Image
src={profile?.banner || DEFAULT_BANNER}
alt="user's banner"
fill={true}
className="h-full w-full object-cover"
/>
<img src={profile?.banner || DEFAULT_BANNER} alt="user's banner" className="h-58 w-full object-cover" />
</div>
<div className="relative -top-8 z-10 px-4">
<div className="relative h-16 w-16 rounded-lg bg-zinc-900 ring-2 ring-zinc-900">
<ImageWithFallback
src={profile?.picture || DEFAULT_AVATAR}
alt={id}
fill={true}
className="rounded-lg object-cover"
/>
<img src={profile?.picture || DEFAULT_AVATAR} alt={id} className="h-16 w-16 rounded-lg object-cover" />
</div>
</div>
</div>

View File

@ -1,5 +1,3 @@
'use client';
import { NoteBase } from '@components/note/base';
import { RelayContext } from '@components/relaysProvider';

View File

@ -1,5 +1,3 @@
'use client';
import { DEFAULT_RELAYS } from '@stores/constants';
import { RelayPool } from 'nostr-relaypool';

View File

@ -1,5 +1,3 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
@ -13,12 +11,7 @@ export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
return (
<div className="flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
<ImageWithFallback
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
fill={true}
className="rounded-full object-cover"
/>
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-full object-cover" />
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-zinc-200">

View File

@ -1,5 +1,3 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
@ -16,12 +14,7 @@ export const UserExtend = ({ pubkey, time }: { pubkey: string; time: number }) =
return (
<div className="group flex h-11 items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<ImageWithFallback
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
fill={true}
className="rounded-md object-cover"
/>
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-md object-cover" />
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex flex-col gap-1">

View File

@ -1,5 +1,3 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
@ -11,12 +9,7 @@ export const UserFollow = ({ pubkey }: { pubkey: string }) => {
return (
<div className="flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
<ImageWithFallback
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
fill={true}
className="rounded-full object-cover"
/>
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-full object-cover" />
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-zinc-200">

View File

@ -1,5 +1,3 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
@ -17,11 +15,10 @@ export const UserLarge = ({ pubkey, time }: { pubkey: string; time: number }) =>
return (
<div className="flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<ImageWithFallback
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
fill={true}
className="rounded-md border border-white/10 object-cover"
className="h-11 w-11 rounded-md border border-white/10 object-cover"
/>
</div>
<div className="w-full flex-1">

View File

@ -1,5 +1,3 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
@ -11,12 +9,7 @@ export const UserMini = ({ pubkey }: { pubkey: string }) => {
return (
<div className="group flex items-start gap-1">
<div className="relative h-7 w-7 shrink overflow-hidden rounded border border-white/10">
<ImageWithFallback
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
fill={true}
className="rounded object-cover"
/>
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-7 w-7 rounded object-cover" />
</div>
<span className="text-xs font-medium leading-none text-zinc-500">
Replying to {profile?.name || shortenKey(pubkey)}

View File

@ -1,5 +1,3 @@
import { ImageWithFallback } from '@components/imageWithFallback';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
@ -16,12 +14,7 @@ export const UserQuoteRepost = ({ pubkey, time }: { pubkey: string; time: number
return (
<div className="group flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<ImageWithFallback
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
fill={true}
className="rounded-md object-cover"
/>
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-md object-cover" />
</div>
<div className="flex items-baseline gap-2 text-sm">
<h5 className="font-semibold leading-tight group-hover:underline">

View File

@ -1,24 +1,25 @@
'use client';
import { ChannelMessages } from '@components/channels/messages';
import { FormChannel } from '@components/form/channel';
import NewsfeedLayout from '@components/layouts/newsfeed';
import { RelayContext } from '@components/relaysProvider';
import { channelMessagesAtom, channelReplyAtom } from '@stores/channel';
import { FULL_RELAYS } from '@stores/constants';
import { dateToUnix, hoursAgo } from '@utils/getDate';
import { usePageContext } from '@utils/hooks/usePageContext';
import useLocalStorage from '@rehooks/local-storage';
import { useSetAtom } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { useSearchParams } from 'next/navigation';
import { useContext, useEffect, useRef } from 'react';
import useSWRSubscription from 'swr/subscription';
export default function Page() {
const searchParams = useSearchParams();
const id = searchParams.get('channel-id');
export function Page() {
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const id = searchParams.id;
const [pool]: any = useContext(RelayContext);
const [activeAccount]: any = useLocalStorage('account', {});
@ -84,11 +85,13 @@ export default function Page() {
}, [resetChannelReply, resetChannelMessages]);
return (
<div className="flex h-full w-full flex-col justify-between">
<ChannelMessages />
<div className="shrink-0 p-3">
<FormChannel eventId={id} />
<NewsfeedLayout>
<div className="flex h-full w-full flex-col justify-between">
<ChannelMessages />
<div className="shrink-0 p-3">
<FormChannel eventId={id} />
</div>
</div>
</div>
</NewsfeedLayout>
);
}

View File

@ -1,22 +1,24 @@
'use client';
import { MessageList } from '@components/chats/messageList';
import FormChat from '@components/form/chat';
import NewsfeedLayout from '@components/layouts/newsfeed';
import { RelayContext } from '@components/relaysProvider';
import { chatMessagesAtom } from '@stores/chat';
import { FULL_RELAYS } from '@stores/constants';
import { usePageContext } from '@utils/hooks/usePageContext';
import useLocalStorage from '@rehooks/local-storage';
import { useSetAtom } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { useSearchParams } from 'next/navigation';
import { useContext, useEffect } from 'react';
import useSWRSubscription from 'swr/subscription';
export default function Page() {
const searchParams = useSearchParams();
const pubkey = searchParams.get('pubkey');
export function Page() {
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const pubkey = searchParams.pubkey;
const [pool]: any = useContext(RelayContext);
const [activeAccount]: any = useLocalStorage('account', {});
@ -62,11 +64,13 @@ export default function Page() {
}, [resetChatMessages]);
return (
<div className="flex h-full w-full flex-col justify-between">
<MessageList />
<div className="shrink-0 p-3">
<FormChat receiverPubkey={pubkey} />
<NewsfeedLayout>
<div className="flex h-full w-full flex-col justify-between">
<MessageList />
<div className="shrink-0 p-3">
<FormChat receiverPubkey={pubkey} />
</div>
</div>
</div>
</NewsfeedLayout>
);
}

View File

@ -1,5 +1,3 @@
'use client';
import { RelayContext } from '@components/relaysProvider';
import { dateToUnix, hoursAgo } from '@utils/getDate';
@ -19,11 +17,10 @@ import { getParentID } from '@utils/transform';
import LumeSymbol from '@assets/icons/Lume';
import { writeStorage } from '@rehooks/local-storage';
import { useRouter } from 'next/navigation';
import { useCallback, useContext, useEffect, useRef } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
export default function Page() {
const router = useRouter();
export function Page() {
const [pool, relays]: any = useContext(RelayContext);
const now = useRef(new Date());
@ -126,7 +123,7 @@ export default function Page() {
() => {
updateLastLogin(dateToUnix(now.current));
timeout.current = setTimeout(() => {
router.replace('/nostr/newsfeed/following', { forceOptimisticNavigation: true });
navigate('/newsfeed/following', { overwriteLastHistoryEntry: true });
}, 5000);
},
{
@ -139,39 +136,41 @@ export default function Page() {
unsubscribe();
};
},
[router, pool, relays]
[pool, relays]
);
useEffect(() => {
let ignore = false;
getPlebs()
.then((res) => {
if (res && !ignore) {
writeStorage('plebs', res);
}
})
.catch(console.error);
if (!ignore) {
getActiveAccount()
.then((res: any) => {
if (res) {
const account = res;
// update local storage
writeStorage('account', account);
// fetch data
fetchData(account, account.follows);
} else {
navigate('/onboarding', { overwriteLastHistoryEntry: true });
}
})
.catch(console.error);
getActiveAccount()
.then((res: any) => {
if (res && !ignore) {
const account = res;
// update local storage
writeStorage('account', account);
// fetch data
fetchData(account, account.follows);
} else {
router.replace('/onboarding', { forceOptimisticNavigation: true });
}
})
.catch(console.error);
getPlebs()
.then((res) => {
if (res && !ignore) {
writeStorage('plebs', res);
}
})
.catch(console.error);
}
return () => {
ignore = true;
clearTimeout(timeout.current);
};
}, [fetchData, router]);
}, [fetchData]);
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">

View File

@ -0,0 +1,3 @@
export function Page() {
return <></>;
}

View File

@ -1,6 +1,5 @@
'use client';
import FormBase from '@components/form/base';
import NewsfeedLayout from '@components/layouts/newsfeed';
import { NoteBase } from '@components/note/base';
import { Placeholder } from '@components/note/placeholder';
import { NoteQuoteRepost } from '@components/note/quoteRepost';
@ -15,7 +14,7 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useEffect, useRef } from 'react';
import { Virtuoso } from 'react-virtuoso';
export default function Page() {
export function Page() {
const [hasNewerNote, setHasNewerNote] = useAtom(hasNewerNoteAtom);
const setData = useSetAtom(notesAtom);
const data = useAtomValue(filteredNotesAtom);
@ -82,29 +81,31 @@ export default function Page() {
}, [initialData]);
return (
<div className="relative h-full w-full">
{hasNewerNote && (
<div className="absolute left-1/2 top-2 z-50 -translate-x-1/2 transform">
<button
onClick={() => loadLatest()}
className="inline-flex h-8 transform items-center justify-center gap-1 rounded-full bg-fuchsia-500 pl-3 pr-3.5 text-sm shadow-md shadow-fuchsia-800/20 active:translate-y-1"
>
<ArrowUp width={14} height={14} />
Load latest
</button>
</div>
)}
<Virtuoso
ref={virtuosoRef}
data={data}
itemContent={itemContent}
computeItemKey={computeItemKey}
components={COMPONENTS}
overscan={200}
endReached={loadMore}
className="scrollbar-hide h-full w-full overflow-y-auto"
/>
</div>
<NewsfeedLayout>
<div className="relative h-full w-full">
{hasNewerNote && (
<div className="absolute left-1/2 top-2 z-50 -translate-x-1/2 transform">
<button
onClick={() => loadLatest()}
className="inline-flex h-8 transform items-center justify-center gap-1 rounded-full bg-fuchsia-500 pl-3 pr-3.5 text-sm shadow-md shadow-fuchsia-800/20 active:translate-y-1"
>
<ArrowUp width={14} height={14} />
Load latest
</button>
</div>
)}
<Virtuoso
ref={virtuosoRef}
data={data}
itemContent={itemContent}
computeItemKey={computeItemKey}
components={COMPONENTS}
overscan={200}
endReached={loadMore}
className="scrollbar-hide h-full w-full overflow-y-auto"
/>
</div>
</NewsfeedLayout>
);
}

View File

@ -0,0 +1,172 @@
import OnboardingLayout from '@components/layouts/onboarding';
import { RelayContext } from '@components/relaysProvider';
import { createAccount } from '@utils/storage';
import { EyeClose, EyeEmpty } from 'iconoir-react';
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
import { useCallback, useContext, useMemo, useState } from 'react';
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
import { navigate } from 'vite-plugin-ssr/client/router';
const config: Config = {
dictionaries: [names],
};
export function Page() {
const [pool, relays]: any = useContext(RelayContext);
const [type, setType] = useState('password');
const [loading, setLoading] = useState(false);
const privkey = useMemo(() => generatePrivateKey(), []);
const name = useMemo(() => uniqueNamesGenerator(config).toString(), []);
const pubkey = getPublicKey(privkey);
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(privkey);
// auto-generated profile metadata
const metadata: any = useMemo(
() => ({
display_name: name,
name: name,
username: name.toLowerCase(),
picture: 'https://void.cat/d/KmypFh2fBdYCEvyJrPiN89.webp',
}),
[name]
);
// toggle privatek key
const showPrivateKey = () => {
if (type === 'password') {
setType('text');
} else {
setType('password');
}
};
// create account and broadcast to all relays
const submit = useCallback(async () => {
setLoading(true);
// build event
const event: any = {
content: JSON.stringify(metadata),
created_at: Math.floor(Date.now() / 1000),
kind: 0,
pubkey: pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, privkey);
// insert to database
createAccount(pubkey, privkey, metadata);
// broadcast
pool.publish(event, relays);
// redirect to next step
navigate(`/onboarding/create/step-2?pubkey=${pubkey}&privkey=${privkey}`);
}, [pool, pubkey, privkey, metadata, relays]);
return (
<OnboardingLayout>
<div className="grid h-full w-full grid-rows-5">
<div className="row-span-1 mx-auto flex w-full max-w-md items-center justify-center">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Create new account
</h1>
</div>
<div className="row-span-4">
<div className="mx-auto w-full max-w-md">
<div className="mb-8 flex flex-col gap-4">
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">Public Key</label>
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<input
readOnly
value={npub}
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
/>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">Private Key</label>
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<input
readOnly
type={type}
value={nsec}
className="relative w-full rounded-lg border border-black/5 py-2.5 pl-3.5 pr-11 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
/>
<button
onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{type === 'password' ? (
<EyeClose width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
) : (
<EyeEmpty width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
)}
</button>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-zinc-400">Default Profile (you can change it later)</label>
<div className="relative w-full shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<div className="relative w-full rounded-lg border border-black/5 px-3.5 py-4 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600">
<div className="flex space-x-2">
<div className="relative h-11 w-11 rounded-md">
<img
src={metadata.picture}
alt="default avatar"
className="h-11 w-11 rounded-md object-cover"
/>
</div>
<div className="flex-1 space-y-2 py-1">
<div className="flex items-center gap-1">
<p className="font-semibold">{metadata.display_name}</p>
<p className="text-zinc-400">@{metadata.username}</p>
</div>
<div className="space-y-1">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
</div>
<div className="h-2 rounded bg-zinc-700"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="flex h-10 items-center justify-center">
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<button
onClick={() => submit()}
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Continue </span>
</button>
)}
</div>
</div>
</div>
</div>
</OnboardingLayout>
);
}

View File

@ -1,17 +1,17 @@
'use client';
import OnboardingLayout from '@components/layouts/onboarding';
import { RelayContext } from '@components/relaysProvider';
import { UserBase } from '@components/user/base';
import { usePageContext } from '@utils/hooks/usePageContext';
import { fetchProfileMetadata } from '@utils/hooks/useProfileMetadata';
import { createPleb, updateAccount } from '@utils/storage';
import { arrayToNIP02 } from '@utils/transform';
import { createClient } from '@supabase/supabase-js';
import { CheckCircle } from 'iconoir-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { getEventHash, signEvent } from 'nostr-tools';
import { Key, useCallback, useContext, useEffect, useState } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
const supabase = createClient(
'https://niwaazauwnrwiwmnocnn.supabase.co',
@ -53,12 +53,12 @@ const initialList = [
{ pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609' },
];
export default function Page() {
const router = useRouter();
const searchParams = useSearchParams();
export function Page() {
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const pubkey = searchParams.get('pubkey');
const privkey = searchParams.get('privkey');
const pubkey = searchParams.pubkey;
const privkey = searchParams.privkey;
const [pool, relays]: any = useContext(RelayContext);
const [loading, setLoading] = useState(false);
@ -100,8 +100,8 @@ export default function Page() {
// broadcast
pool.publish(event, relays);
// redirect to splashscreen
router.replace('/');
}, [pubkey, privkey, follows, pool, relays, router]);
navigate('/');
}, [pubkey, privkey, follows, pool, relays]);
useEffect(() => {
let ignore = false;
@ -122,65 +122,67 @@ export default function Page() {
}, []);
return (
<div className="relative grid h-full w-full grid-rows-5">
<div className="row-span-1 flex items-center justify-center">
<div className="text-center">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium leading-tight text-transparent">
Personalized your newsfeed
</h1>
<h3 className="text-lg text-zinc-500">
Follow at least{' '}
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
{follows.length}/10
</span>{' '}
plebs
</h3>
<OnboardingLayout>
<div className="relative grid h-full w-full grid-rows-5">
<div className="row-span-1 flex items-center justify-center">
<div className="text-center">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium leading-tight text-transparent">
Personalized your newsfeed
</h1>
<h3 className="text-lg text-zinc-500">
Follow at least{' '}
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
{follows.length}/10
</span>{' '}
plebs
</h3>
</div>
</div>
</div>
<div className="row-span-4 h-full w-full overflow-y-auto">
<div className="grid grid-cols-4 gap-4 px-8 py-4">
{list.map((item: { pubkey: string }, index: Key) => (
<div className="row-span-4 h-full w-full overflow-y-auto">
<div className="grid grid-cols-4 gap-4 px-8 py-4">
{list.map((item: { pubkey: string }, index: Key) => (
<button
key={index}
onClick={() => toggleFollow(item.pubkey)}
className="flex transform items-center justify-between rounded-lg bg-zinc-900 p-2 ring-amber-100 hover:ring-1 active:translate-y-1"
>
<UserBase pubkey={item.pubkey} />
{follows.includes(item.pubkey) && (
<div>
<CheckCircle width={16} height={16} className="text-zinc-400" />
</div>
)}
</button>
))}
</div>
</div>
{follows.length >= 10 && (
<div className="fixed bottom-0 left-0 z-10 flex h-24 w-full items-center justify-center">
<button
key={index}
onClick={() => toggleFollow(item.pubkey)}
className="flex transform items-center justify-between rounded-lg bg-zinc-900 p-2 ring-amber-100 hover:ring-1 active:translate-y-1"
onClick={() => submit()}
className="relative z-20 inline-flex w-36 transform items-center justify-center rounded-full bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 shadow-xl active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<UserBase pubkey={item.pubkey} />
{follows.includes(item.pubkey) && (
<div>
<CheckCircle width={16} height={16} className="text-zinc-400" />
</div>
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-zinc-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<span className="drop-shadow-lg">Done! Go to newsfeed</span>
)}
</button>
))}
</div>
</div>
)}
</div>
{follows.length >= 10 && (
<div className="fixed bottom-0 left-0 z-10 flex h-24 w-full items-center justify-center">
<button
onClick={() => submit()}
className="relative z-20 inline-flex w-36 transform items-center justify-center rounded-full bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 shadow-xl active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
{loading === true ? (
<svg
className="h-5 w-5 animate-spin text-zinc-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<span className="drop-shadow-lg">Done! Go to newsfeed</span>
)}
</button>
</div>
)}
</div>
</OnboardingLayout>
);
}

View File

@ -1,6 +1,6 @@
import OnboardingLayout from '@components/layouts/onboarding';
import { ArrowRight } from 'iconoir-react';
import Image from 'next/image';
import Link from 'next/link';
const PLEBS = [
'https://133332.xyz/p.jpg',
@ -72,58 +72,47 @@ const InfiniteLoopSlider = ({ children, duration, reverse }: { children: any; du
);
};
export default function Page() {
export function Page() {
return (
<div className="grid h-full w-full grid-rows-5">
<div className="row-span-3 overflow-hidden">
<div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4">
{[...new Array(ROWS)].map((_, i) => (
<InfiniteLoopSlider key={i} duration={random(DURATION - 5000, DURATION + 20000)} reverse={i % 2}>
{shuffle(PLEBS)
.slice(0, PLEBS_PER_ROW)
.map((tag) => (
<div
key={tag}
className="relative mr-4 flex h-11 w-11 items-center gap-2 rounded-md bg-zinc-900 px-4 py-1.5 shadow-xl"
>
<Image
src={tag}
alt={tag}
fill={true}
className="rounded-md border border-zinc-900"
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
priority
/>
</div>
))}
</InfiniteLoopSlider>
))}
<div className="pointer-events-none absolute inset-0 bg-fade" />
<OnboardingLayout>
<div className="grid h-full w-full grid-rows-5">
<div className="row-span-3 overflow-hidden">
<div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4">
{[...new Array(ROWS)].map((_, i) => (
<InfiniteLoopSlider key={i} duration={random(DURATION - 5000, DURATION + 20000)} reverse={i % 2}>
{shuffle(PLEBS)
.slice(0, PLEBS_PER_ROW)
.map((tag) => (
<div key={tag} className="relative mr-4 h-11 w-11 gap-2 rounded-md bg-zinc-900 shadow-xl">
<img src={tag} alt={tag} className="h-11 w-11 rounded-md border border-zinc-900" />
</div>
))}
</InfiniteLoopSlider>
))}
<div className="pointer-events-none absolute inset-0 bg-fade" />
</div>
</div>
<div className="row-span-2 flex w-full flex-col items-center gap-4 overflow-hidden pt-6 min-[1050px]:gap-8 min-[1050px]:pt-10">
<h1 className="animate-moveBg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-5xl font-bold leading-none text-transparent">
Let&apos;s start!
</h1>
<div className="mt-4 flex flex-col items-center gap-1.5">
<a
href="/onboarding/create"
className="relative inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full bg-zinc-900 px-6 text-lg font-medium ring-1 ring-zinc-800 hover:bg-zinc-800"
>
Create new key
<ArrowRight width={20} height={20} />
</a>
<a
href="/onboarding/login"
className="inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full px-6 text-base font-medium text-zinc-300 hover:bg-zinc-800"
>
Login with private key
</a>
</div>
</div>
</div>
<div className="row-span-2 flex w-full flex-col items-center gap-4 overflow-hidden pt-6 min-[1050px]:gap-8 min-[1050px]:pt-10">
<h1 className="animate-moveBg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-5xl font-bold leading-none text-transparent">
Let&apos;s start!
</h1>
<div className="mt-4 flex flex-col items-center gap-1.5">
<Link
prefetch={false}
href="/onboarding/create"
className="relative inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full bg-zinc-900 px-6 text-lg font-medium ring-1 ring-zinc-800 hover:bg-zinc-800"
>
Create new key
<ArrowRight width={20} height={20} />
</Link>
<Link
prefetch={false}
href="/onboarding/login"
className="inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full px-6 text-base font-medium text-zinc-300 hover:bg-zinc-800"
>
Login with private key
</Link>
</div>
</div>
</div>
</OnboardingLayout>
);
}

View File

@ -0,0 +1,121 @@
import OnboardingLayout from '@components/layouts/onboarding';
import { CableTag } from 'iconoir-react';
import { getPublicKey, nip19 } from 'nostr-tools';
import { Resolver, useForm } from 'react-hook-form';
import { navigate } from 'vite-plugin-ssr/client/router';
type FormValues = {
key: string;
};
const resolver: Resolver<FormValues> = async (values) => {
return {
values: values.key ? values : {},
errors: !values.key
? {
key: {
type: 'required',
message: 'This is required.',
},
}
: {},
};
};
export function Page() {
const {
register,
setError,
handleSubmit,
formState: { errors, isDirty, isValid, isSubmitting },
} = useForm<FormValues>({ resolver });
const onSubmit = async (data: any) => {
try {
let privkey = data['key'];
if (privkey.substring(0, 4) === 'nsec') {
privkey = nip19.decode(privkey).data;
}
if (typeof getPublicKey(privkey) === 'string') {
navigate(`/onboarding/login/step-2?privkey=${privkey}`);
}
} catch (error) {
setError('key', {
type: 'custom',
message: 'Private Key is invalid, please check again',
});
}
};
return (
<OnboardingLayout>
<div className="grid h-full w-full grid-rows-5">
<div className="row-span-1 mx-auto flex w-full max-w-md items-center justify-center">
<h1 className="bg-gradient-to-br from-zinc-200 via-white to-zinc-300 bg-clip-text text-3xl font-semibold text-transparent">
Login with Private Key
</h1>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="row-span-4">
<div className="mx-auto w-full max-w-md">
<div className="flex flex-col gap-4">
<div>
{/* #TODO: add function */}
<button className="inline-flex w-full transform items-center justify-center gap-1.5 rounded-lg bg-zinc-700 px-3.5 py-2.5 font-medium text-zinc-200 shadow-input ring-1 ring-zinc-600 active:translate-y-1">
{/* #TODO: change to nostr connect logo */}
<CableTag width={20} height={20} className="text-fuchsia-500" />
<span>Continue with Nostr Connect</span>
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-zinc-800"></div>
</div>
<div className="relative flex justify-center">
<span className="bg-black px-2 text-sm text-zinc-500">or</span>
</div>
</div>
<div className="flex flex-col gap-0.5">
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<input
{...register('key', { required: true, minLength: 32 })}
type={'password'}
placeholder="Paste private key here..."
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 text-center shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<span className="text-xs text-red-400">{errors.key && <p>{errors.key.message}</p>}</span>
</div>
</div>
<div className="mt-3 flex h-10 items-center justify-center">
{isSubmitting ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<button
type="submit"
disabled={!isDirty || !isValid}
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Continue </span>
</button>
)}
</div>
</div>
</form>
</div>
</OnboardingLayout>
);
}

View File

@ -0,0 +1,144 @@
import OnboardingLayout from '@components/layouts/onboarding';
import { RelayContext } from '@components/relaysProvider';
import { DEFAULT_AVATAR } from '@stores/constants';
import { usePageContext } from '@utils/hooks/usePageContext';
import { fetchProfileMetadata } from '@utils/hooks/useProfileMetadata';
import { shortenKey } from '@utils/shortenKey';
import { createAccount, createPleb, updateAccount } from '@utils/storage';
import { nip02ToArray } from '@utils/transform';
import { getPublicKey } from 'nostr-tools';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
export function Page() {
const pageContext = usePageContext();
const searchParams = pageContext.urlParsed.search;
const privkey = searchParams.privkey;
const pubkey = getPublicKey(privkey);
const [pool, relays]: any = useContext(RelayContext);
const [profile, setProfile] = useState({ metadata: null });
const [done, setDone] = useState(false);
const timeout = useRef(null);
const createPlebs = useCallback(async (tags: string[]) => {
for (const tag of tags) {
fetchProfileMetadata(tag[1])
.then((res: any) => createPleb(tag[1], res.content))
.catch(console.error);
}
}, []);
useEffect(() => {
const unsubscribe = pool.subscribe(
[
{
kinds: [0, 3],
authors: [pubkey],
},
],
relays,
(event: any) => {
if (event.kind === 0) {
// create account
createAccount(pubkey, privkey, event.content);
// update state
setProfile({
metadata: JSON.parse(event.content),
});
} else {
if (event.tags.length > 0) {
createPlebs(event.tags);
const arr = nip02ToArray(event.tags);
// update account's folllows with NIP-02 tag list
updateAccount('follows', arr, pubkey);
}
}
},
undefined,
() => {
timeout.current = setTimeout(() => setDone(true), 5000);
},
{
unsubscribeOnEose: true,
logAllEvents: false,
}
);
return () => {
unsubscribe();
clearTimeout(timeout.current);
};
}, [pool, relays, pubkey, privkey, createPlebs]);
return (
<OnboardingLayout>
<div className="grid h-full w-full grid-rows-5">
<div className="row-span-1 flex items-center justify-center">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Bringing back your profile...
</h1>
</div>
<div className="row-span-4 flex flex-col gap-8">
<div className="mx-auto w-full max-w-md">
<div className="mb-4 flex flex-col gap-2">
<div className="w-full rounded-lg bg-zinc-900 p-4 shadow-input ring-1 ring-zinc-800">
<div className="flex space-x-4">
<div className="relative h-10 w-10 rounded-full">
<img
className="h-10 w-10 rounded-full object-cover"
src={profile.metadata?.picture || DEFAULT_AVATAR}
alt="avatar"
/>
</div>
<div className="flex-1 space-y-4 py-1">
<div className="flex items-center gap-2">
<p className="font-semibold">{profile.metadata?.display_name || profile.metadata?.name}</p>
<span className="leading-tight text-zinc-500">·</span>
<p className="text-zinc-500">@{profile.metadata?.username || (pubkey && shortenKey(pubkey))}</p>
</div>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
</div>
<div className="h-2 rounded bg-zinc-700"></div>
</div>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-center">
{done === false ? (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<a
href="/"
className="inline-flex w-full transform items-center justify-center rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Done! Go to newsfeed</span>
</a>
)}
</div>
</div>
</div>
</div>
</OnboardingLayout>
);
}

View File

@ -0,0 +1,57 @@
'use client';
import NewsfeedLayout from '@components/layouts/newsfeed';
import ProfileFollowers from '@components/profile/followers';
import ProfileFollows from '@components/profile/follows';
import ProfileMetadata from '@components/profile/metadata';
import ProfileNotes from '@components/profile/notes';
import { usePageContext } from '@utils/hooks/usePageContext';
import * as Tabs from '@radix-ui/react-tabs';
export function Page() {
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const pubkey = searchParams.pubkey;
return (
<NewsfeedLayout>
<div className="scrollbar-hide h-full w-full overflow-y-auto">
<ProfileMetadata id={pubkey} />
<Tabs.Root className="flex w-full flex-col" defaultValue="notes">
<Tabs.List className="flex border-b border-zinc-800">
<Tabs.Trigger
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
value="notes"
>
Notes
</Tabs.Trigger>
<Tabs.Trigger
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium text-zinc-400 outline-none placeholder:leading-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
value="followers"
>
Followers
</Tabs.Trigger>
<Tabs.Trigger
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
value="following"
>
Following
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="notes">
<ProfileNotes id={pubkey} />
</Tabs.Content>
<Tabs.Content value="followers">
<ProfileFollowers id={pubkey} />
</Tabs.Content>
<Tabs.Content value="following">
<ProfileFollows id={pubkey} />
</Tabs.Content>
</Tabs.Root>
</div>
</NewsfeedLayout>
);
}

View File

@ -0,0 +1,20 @@
import '@renderer/index.css';
import { Shell } from '@renderer/shell';
import { PageContextClient } from '@renderer/types';
import { hydrateRoot } from 'react-dom/client';
export const clientRouting = true;
export async function render(pageContext: PageContextClient) {
const { Page, pageProps } = pageContext;
if (!Page) throw new Error('Client-side render() hook expects pageContext.Page to be defined');
hydrateRoot(
document.getElementById('app')!,
<Shell pageContext={pageContext}>
<Page {...pageProps} />
</Shell>
);
}

View File

@ -0,0 +1,30 @@
import { Shell } from '@renderer/shell';
import { PageContextServer } from '@renderer/types';
import ReactDOMServer from 'react-dom/server';
import { dangerouslySkipEscape, escapeInject } from 'vite-plugin-ssr/server';
export const passToClient = ['pageProps'];
export function render(pageContext: PageContextServer) {
const { Page, pageProps } = pageContext;
if (!Page) throw new Error('My render() hook expects pageContext.Page to be defined');
const pageHtml = ReactDOMServer.renderToString(
<Shell pageContext={pageContext}>
<Page {...pageProps} />
</Shell>
);
return escapeInject`<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body class="cursor-default select-none overflow-hidden font-sans antialiased">
<div id="app">${dangerouslySkipEscape(pageHtml)}</div>
</body>
</html>`;
}

View File

@ -0,0 +1,17 @@
export function Page({ is404 }: { is404: boolean }) {
if (is404) {
return (
<>
<h1>404 Page Not Found</h1>
<p>This page could not be found.</p>
</>
);
} else {
return (
<>
<h1>500 Internal Server Error</h1>
<p>Something went wrong.</p>
</>
);
}
}

17
src/renderer/shell.tsx Normal file
View File

@ -0,0 +1,17 @@
import RelayProvider from '@components/relaysProvider';
import { PageContextProvider } from '@utils/hooks/usePageContext';
import { PageContext } from '@renderer/types';
import { StrictMode } from 'react';
export function Shell({ children, pageContext }: { children: React.ReactNode; pageContext: PageContext }) {
return (
<StrictMode>
<PageContextProvider pageContext={pageContext}>
<RelayProvider>{children}</RelayProvider>
</PageContextProvider>
</StrictMode>
);
}

35
src/renderer/types.ts Normal file
View File

@ -0,0 +1,35 @@
import type {
PageContextBuiltIn,
/*
// When using Client Routing https://vite-plugin-ssr.com/clientRouting
PageContextBuiltInClientWithClientRouting as PageContextBuiltInClient
/*/
// When using Server Routing
PageContextBuiltInClientWithServerRouting as PageContextBuiltInClient, //*/
} from 'vite-plugin-ssr/types';
export type { PageContextServer };
export type { PageContextClient };
export type { PageContext };
export type { PageProps };
type Page = (pageProps: PageProps) => React.ReactElement;
type PageProps = Record<string, never>;
export type PageContextCustom = {
Page: Page;
pageProps?: PageProps;
redirectTo?: string;
urlPathname: string;
exports: {
documentProps?: {
title?: string;
description?: string;
};
};
};
type PageContextServer = PageContextBuiltIn<Page> & PageContextCustom;
type PageContextClient = PageContextBuiltInClient<Page> & PageContextCustom;
type PageContext = PageContextClient | PageContextServer;

View File

@ -0,0 +1,22 @@
// `usePageContext` allows us to access `pageContext` in any React component.
// See https://vite-plugin-ssr.com/pageContext-anywhere
import type { PageContext } from '@renderer/types';
import { createContext, useContext } from 'react';
const Context = createContext<PageContext>(undefined as any);
export function PageContextProvider({
pageContext,
children,
}: {
pageContext: PageContext;
children: React.ReactNode;
}) {
return <Context.Provider value={pageContext}>{children}</Context.Provider>;
}
export function usePageContext() {
const pageContext = useContext(Context);
return pageContext;
}

View File

@ -6,7 +6,8 @@
"@components/*": ["src/components/*"],
"@stores/*": ["src/stores/*"],
"@utils/*": ["src/utils/*"],
"@assets/*": ["src/assets/*"]
"@assets/*": ["src/assets/*"],
"@renderer/*": ["src/renderer/*"]
},
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],

View File

@ -4,5 +4,5 @@ import ssr from 'vite-plugin-ssr/plugin';
import viteTsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), ssr(), viteTsconfigPaths()],
plugins: [react(), ssr({ prerender: true }), viteTsconfigPaths()],
});