mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
migrated to vite and vite-plugin-ssr
This commit is contained in:
parent
c50e507c14
commit
b9bafc851e
@ -15,6 +15,7 @@
|
|||||||
"^@stores/(.*)$",
|
"^@stores/(.*)$",
|
||||||
"^@utils/(.*)$",
|
"^@utils/(.*)$",
|
||||||
"^@assets/(.*)$",
|
"^@assets/(.*)$",
|
||||||
|
"^@renderer/(.*)$",
|
||||||
"<THIRD_PARTY_MODULES>",
|
"<THIRD_PARTY_MODULES>",
|
||||||
"^[./]"
|
"^[./]"
|
||||||
],
|
],
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"beforeBuildCommand": "pnpm build",
|
"beforeBuildCommand": "pnpm build",
|
||||||
"devPath": "http://localhost:1420",
|
"devPath": "http://localhost:3000",
|
||||||
"distDir": "../out",
|
"distDir": "../dist",
|
||||||
"withGlobalTauri": true
|
"withGlobalTauri": true
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>;
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>;
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,24 +1,20 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { platform } from '@tauri-apps/api/os';
|
import { platform } from '@tauri-apps/api/os';
|
||||||
import { ArrowLeft, ArrowRight, Refresh } from 'iconoir-react';
|
import { ArrowLeft, ArrowRight, Refresh } from 'iconoir-react';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useLayoutEffect, useState } from 'react';
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function AppActions() {
|
export default function AppActions() {
|
||||||
const router = useRouter();
|
|
||||||
const [os, setOS] = useState('');
|
const [os, setOS] = useState('');
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.back();
|
window.history.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
const goForward = () => {
|
const goForward = () => {
|
||||||
router.forward();
|
window.history.forward();
|
||||||
};
|
};
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
router.refresh();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import dynamic from 'next/dynamic';
|
import AppActions from '@components/appHeader/actions';
|
||||||
|
import EventCollector from '@components/eventCollector';
|
||||||
const AppActions = dynamic(() => import('@components/appHeader/actions'), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const EventCollector = dynamic(() => import('@components/eventCollector'), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function AppHeader({ collector }: { collector: boolean }) {
|
export default function AppHeader({ collector }: { collector: boolean }) {
|
||||||
return (
|
return (
|
||||||
|
@ -10,19 +10,6 @@ export default function ChannelList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-px">
|
<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) => (
|
{list.map((item) => (
|
||||||
<ChannelListItem key={item.event_id} data={item} />
|
<ChannelListItem key={item.event_id} data={item} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,31 +1,21 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useChannelMetadata } from '@utils/hooks/useChannelMetadata';
|
import { useChannelMetadata } from '@utils/hooks/useChannelMetadata';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export const ChannelListItem = ({ data }: { data: any }) => {
|
export const ChannelListItem = ({ data }: { data: any }) => {
|
||||||
const channel = useChannelMetadata(data.event_id, data.metadata);
|
const channel = useChannelMetadata(data.event_id, data.metadata);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<a
|
||||||
prefetch={false}
|
href={`channel?id=${data.event_id}`}
|
||||||
href={`/nostr/channel?channel-id=${data.event_id}`}
|
|
||||||
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
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">
|
<div className="relative h-5 w-5 shrink-0 rounded">
|
||||||
<ImageWithFallback
|
<img src={channel?.picture || DEFAULT_AVATAR} alt={data.event_id} className="h-5 w-5 rounded object-cover" />
|
||||||
src={channel?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={data.event_id}
|
|
||||||
fill={true}
|
|
||||||
className="rounded object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="truncate text-sm font-medium text-zinc-400">{channel?.name.toLowerCase()}</h5>
|
<h5 className="truncate text-sm font-medium text-zinc-400">{channel?.name.toLowerCase()}</h5>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { ChatListItem } from '@components/chats/chatListItem';
|
import { ChatListItem } from '@components/chats/chatListItem';
|
||||||
import { ChatModal } from '@components/chats/chatModal';
|
import { ChatModal } from '@components/chats/chatModal';
|
||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { getChats } from '@utils/storage';
|
import { getChats } from '@utils/storage';
|
||||||
|
|
||||||
import useLocalStorage from '@rehooks/local-storage';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function ChatList() {
|
export default function ChatList() {
|
||||||
@ -33,17 +31,15 @@ export default function ChatList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-px">
|
<div className="flex flex-col gap-px">
|
||||||
<Link
|
<a
|
||||||
prefetch={false}
|
href={`/chat?pubkey=${activeAccount.pubkey}`}
|
||||||
href={`/nostr/chat?pubkey=${activeAccount.pubkey}`}
|
|
||||||
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
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">
|
<div className="relative h-5 w-5 shrink rounded bg-white">
|
||||||
<ImageWithFallback
|
<img
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
src={profile?.picture || DEFAULT_AVATAR}
|
||||||
alt={activeAccount.pubkey}
|
alt={activeAccount.pubkey}
|
||||||
fill={true}
|
className="h-5 w-5 rounded object-cover"
|
||||||
className="rounded object-cover"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -51,7 +47,7 @@ export default function ChatList() {
|
|||||||
{profile?.display_name || profile?.name} <span className="text-zinc-500">(you)</span>
|
{profile?.display_name || profile?.name} <span className="text-zinc-500">(you)</span>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
{list.map((item) => (
|
{list.map((item) => (
|
||||||
<ChatListItem key={item.id} pubkey={item.pubkey} />
|
<ChatListItem key={item.id} pubkey={item.pubkey} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,34 +1,24 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
import { shortenKey } from '@utils/shortenKey';
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export const ChatListItem = ({ pubkey }: { pubkey: string }) => {
|
export const ChatListItem = ({ pubkey }: { pubkey: string }) => {
|
||||||
const profile = useProfileMetadata(pubkey);
|
const profile = useProfileMetadata(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<a
|
||||||
prefetch={false}
|
href={`/chat?pubkey=${pubkey}`}
|
||||||
href={`/nostr/chat?pubkey=${pubkey}`}
|
|
||||||
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
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">
|
<div className="relative h-5 w-5 shrink rounded">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-5 w-5 rounded object-cover" />
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
fill={true}
|
|
||||||
className="rounded object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-sm font-medium text-zinc-400">
|
<h5 className="text-sm font-medium text-zinc-400">
|
||||||
{profile?.display_name || profile?.name || shortenKey(pubkey)}
|
{profile?.display_name || profile?.name || shortenKey(pubkey)}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,28 +1,24 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { shortenKey } from '@utils/shortenKey';
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
export const ChatModalUser = ({ data }: { data: any }) => {
|
export const ChatModalUser = ({ data }: { data: any }) => {
|
||||||
const router = useRouter();
|
|
||||||
const profile = JSON.parse(data.metadata);
|
const profile = JSON.parse(data.metadata);
|
||||||
|
|
||||||
const openNewChat = () => {
|
const openNewChat = () => {
|
||||||
router.push(`/nostr/chat?pubkey=${data.pubkey}`, { forceOptimisticNavigation: true });
|
navigate(`/chat?pubkey=${data.pubkey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex items-center justify-between px-3 py-2 hover:bg-zinc-800">
|
<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="flex items-center gap-2">
|
||||||
<div className="relative h-10 w-10 shrink overflow-hidden rounded-md">
|
<div className="relative h-10 w-10 shrink-0 rounded-md">
|
||||||
<ImageWithFallback
|
<img
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
src={profile?.picture || DEFAULT_AVATAR}
|
||||||
alt={data.pubkey}
|
alt={data.pubkey}
|
||||||
fill={true}
|
className="h-10 w-10 rounded-md object-cover"
|
||||||
className="rounded-md object-cover"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
@ -15,13 +13,8 @@ export const MessageUser = ({ pubkey, time }: { pubkey: string; time: number })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex items-start gap-3">
|
<div className="group flex items-start gap-3">
|
||||||
<div className="relative h-9 w-9 shrink overflow-hidden rounded-md">
|
<div className="relative h-9 w-9 shrink rounded-md">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-9 w-9 rounded-md object-cover" />
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
fill={true}
|
|
||||||
className="rounded-md object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
<div className="flex items-baseline gap-2 text-sm">
|
<div className="flex items-baseline gap-2 text-sm">
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { NetworkStatusIndicator } from '@components/networkStatusIndicator';
|
import { NetworkStatusIndicator } from '@components/networkStatusIndicator';
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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 />
|
|
||||||
);
|
|
||||||
});
|
|
@ -2,7 +2,7 @@ import AppHeader from '@components/appHeader';
|
|||||||
import MultiAccounts from '@components/multiAccounts';
|
import MultiAccounts from '@components/multiAccounts';
|
||||||
import Navigation from '@components/navigation';
|
import Navigation from '@components/navigation';
|
||||||
|
|
||||||
export default function NostrLayout({ children }: { children: React.ReactNode }) {
|
export default function NewsfeedLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
|
<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">
|
<div className="flex h-screen w-full flex-col">
|
12
src/components/link.tsx
Normal file
12
src/components/link.tsx
Normal 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} />;
|
||||||
|
}
|
@ -1,20 +1,16 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import { writeText } from '@tauri-apps/api/clipboard';
|
import { writeText } from '@tauri-apps/api/clipboard';
|
||||||
import { LogOut, ProfileCircle, Settings } from 'iconoir-react';
|
import { LogOut, ProfileCircle, Settings } from 'iconoir-react';
|
||||||
import Image from 'next/image';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
export const ActiveAccount = ({ user }: { user: any }) => {
|
export const ActiveAccount = ({ user }: { user: any }) => {
|
||||||
const router = useRouter();
|
|
||||||
const userData = JSON.parse(user.metadata);
|
const userData = JSON.parse(user.metadata);
|
||||||
|
|
||||||
const openProfilePage = () => {
|
const openProfilePage = () => {
|
||||||
router.push(`/nostr/user?pubkey=${user.pubkey}`, { forceOptimisticNavigation: true });
|
navigate(`/user?pubkey=${user.pubkey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyPublicKey = async () => {
|
const copyPublicKey = async () => {
|
||||||
@ -25,12 +21,10 @@ export const ActiveAccount = ({ user }: { user: any }) => {
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
<button className="relative h-11 w-11 rounded-lg">
|
<button className="relative h-11 w-11 rounded-lg">
|
||||||
<Image
|
<img
|
||||||
src={userData.picture || DEFAULT_AVATAR}
|
src={userData.picture || DEFAULT_AVATAR}
|
||||||
alt="user's avatar"
|
alt="user's avatar"
|
||||||
fill={true}
|
className="h-11 w-11 rounded-lg object-cover"
|
||||||
className="rounded-lg object-cover"
|
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const InactiveAccount = memo(function InactiveAccount({ user }: { user: any }) {
|
export const InactiveAccount = memo(function InactiveAccount({ user }: { user: any }) {
|
||||||
@ -12,13 +11,7 @@ export const InactiveAccount = memo(function InactiveAccount({ user }: { user: a
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-lg">
|
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-lg">
|
||||||
<Image
|
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
|
||||||
src={userData.picture || DEFAULT_AVATAR}
|
|
||||||
alt="user's avatar"
|
|
||||||
fill={true}
|
|
||||||
className="rounded-lg object-cover"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ActiveAccount } from '@components/multiAccounts/activeAccount';
|
import { ActiveAccount } from '@components/multiAccounts/activeAccount';
|
||||||
import { InactiveAccount } from '@components/multiAccounts/inactiveAccount';
|
import { InactiveAccount } from '@components/multiAccounts/inactiveAccount';
|
||||||
|
|
||||||
@ -11,7 +9,6 @@ import LumeSymbol from '@assets/icons/Lume';
|
|||||||
|
|
||||||
import useLocalStorage from '@rehooks/local-storage';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { Plus } from 'iconoir-react';
|
import { Plus } from 'iconoir-react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function MultiAccounts() {
|
export default function MultiAccounts() {
|
||||||
@ -38,21 +35,19 @@ export default function MultiAccounts() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-between px-2 pb-4 pt-3">
|
<div className="flex h-full flex-col items-center justify-between px-2 pb-4 pt-3">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Link
|
<a
|
||||||
prefetch={false}
|
|
||||||
href="/explore"
|
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"
|
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" />
|
<LumeSymbol className="h-6 w-auto text-zinc-400 group-hover:text-zinc-200" />
|
||||||
</Link>
|
</a>
|
||||||
<div>{users.map((user) => renderAccount(user))}</div>
|
<div>{users.map((user) => renderAccount(user))}</div>
|
||||||
<Link
|
<a
|
||||||
prefetch={false}
|
|
||||||
href="/onboarding"
|
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"
|
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" />
|
<Plus width={16} height={16} className="text-zinc-400 group-hover:text-zinc-200" />
|
||||||
</Link>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 text-center">
|
<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">
|
<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">
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import ChannelList from '@components/channels/channelList';
|
import ChannelList from '@components/channels/channelList';
|
||||||
|
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import ChatList from '@components/chats/chatList';
|
import ChatList from '@components/chats/chatList';
|
||||||
|
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ActiveLink } from '@components/activeLink';
|
|
||||||
|
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { Bonfire, NavArrowUp, PeopleTag } from 'iconoir-react';
|
import { Bonfire, NavArrowUp, PeopleTag } from 'iconoir-react';
|
||||||
import { useState } from '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>
|
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Newsfeed</h3>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content className="flex flex-col text-zinc-400">
|
<Collapsible.Content className="flex flex-col text-zinc-400">
|
||||||
<ActiveLink
|
<a
|
||||||
href="/nostr/newsfeed/following"
|
href="/newsfeed/following"
|
||||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
//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"
|
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" />
|
<PeopleTag width={16} height={16} className="text-zinc-500" />
|
||||||
<span>Following</span>
|
<span>Following</span>
|
||||||
</ActiveLink>
|
</a>
|
||||||
<ActiveLink
|
<a
|
||||||
href="/nostr/newsfeed/circle"
|
href="/newsfeed/circle"
|
||||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
//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"
|
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" />
|
<Bonfire width={16} height={16} className="text-zinc-500" />
|
||||||
<span>Circle</span>
|
<span>Circle</span>
|
||||||
</ActiveLink>
|
</a>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
|
@ -4,11 +4,10 @@ import { UserExtend } from '@components/user/extend';
|
|||||||
|
|
||||||
import { contentParser } from '@utils/parser';
|
import { contentParser } from '@utils/parser';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
||||||
const router = useRouter();
|
|
||||||
const content = contentParser(event.content, event.tags);
|
const content = contentParser(event.content, event.tags);
|
||||||
|
|
||||||
const parentNote = () => {
|
const parentNote = () => {
|
||||||
@ -22,13 +21,13 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
|||||||
|
|
||||||
const openUserPage = (e) => {
|
const openUserPage = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
router.push(`/nostr/user?pubkey=${event.pubkey}`, { forceOptimisticNavigation: true });
|
navigate(`/user?pubkey=${event.pubkey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openThread = (e) => {
|
const openThread = (e) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.toString().length === 0) {
|
if (selection.toString().length === 0) {
|
||||||
router.push(`/nostr/newsfeed/note?id=${event.parent_id}`, { forceOptimisticNavigation: true });
|
navigate(`/newsfeed/note?id=${event.parent_id}`);
|
||||||
} else {
|
} else {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
import { UserExtend } from '@components/user/extend';
|
import { UserExtend } from '@components/user/extend';
|
||||||
|
|
||||||
@ -7,9 +6,9 @@ import { dateToUnix } from '@utils/getDate';
|
|||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import useLocalStorage from '@rehooks/local-storage';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { ChatLines, OpenNewWindow } from 'iconoir-react';
|
import { ChatLines, OpenNewWindow } from 'iconoir-react';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
export const NoteComment = ({
|
export const NoteComment = ({
|
||||||
count,
|
count,
|
||||||
@ -24,7 +23,6 @@ export const NoteComment = ({
|
|||||||
eventTime: number;
|
eventTime: number;
|
||||||
eventContent: any;
|
eventContent: any;
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -34,7 +32,7 @@ export const NoteComment = ({
|
|||||||
const profile = activeAccount.metadata ? JSON.parse(activeAccount.metadata) : null;
|
const profile = activeAccount.metadata ? JSON.parse(activeAccount.metadata) : null;
|
||||||
|
|
||||||
const openThread = () => {
|
const openThread = () => {
|
||||||
router.push(`/nostr/newsfeed/note?id=${eventID}`, { forceOptimisticNavigation: true });
|
navigate(`/newsfeed/note?id=${eventID}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitEvent = () => {
|
const submitEvent = () => {
|
||||||
@ -84,12 +82,7 @@ export const NoteComment = ({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture} alt="user's avatar" className="h-11 w-11 rounded-md object-cover" />
|
||||||
src={profile?.picture}
|
|
||||||
alt="user's avatar"
|
|
||||||
fill={true}
|
|
||||||
className="rounded-md object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const ImagePreview = memo(function ImagePreview({ url, size }: { url: string; size: string }) {
|
export const ImagePreview = memo(function ImagePreview({ url, size }: { url: string; size: string }) {
|
||||||
return (
|
return (
|
||||||
<div className={`relative h-full ${size === 'large' ? 'w-4/5' : 'w-1/2'} mt-2 rounded-lg border border-zinc-800`}>
|
<div className={`relative h-full ${size === 'large' ? 'w-4/5' : 'w-1/2'} mt-2 rounded-lg border border-zinc-800`}>
|
||||||
<Image
|
<img src={url} alt={url} className="h-auto w-full rounded-lg object-cover" />
|
||||||
src={url}
|
|
||||||
alt={url}
|
|
||||||
width="0"
|
|
||||||
height="0"
|
|
||||||
sizes="100vw"
|
|
||||||
className="h-auto w-full rounded-lg object-cover"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -4,11 +4,10 @@ import { UserExtend } from '@components/user/extend';
|
|||||||
|
|
||||||
import { contentParser } from '@utils/parser';
|
import { contentParser } from '@utils/parser';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
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 }) {
|
export const RootNote = memo(function RootNote({ event }: { event: any }) {
|
||||||
const router = useRouter();
|
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
@ -16,13 +15,13 @@ export const RootNote = memo(function RootNote({ event }: { event: any }) {
|
|||||||
|
|
||||||
const openUserPage = (e) => {
|
const openUserPage = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
router.push(`/nostr/user?pubkey=${event.pubkey}`, { forceOptimisticNavigation: true });
|
navigate(`/user?pubkey=${event.pubkey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openThread = (e) => {
|
const openThread = (e) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.toString().length === 0) {
|
if (selection.toString().length === 0) {
|
||||||
router.push(`/nostr/newsfeed/note?id=${event.parent_id}`, { forceOptimisticNavigation: true });
|
navigate(`/newsfeed/note?id=${event.parent_id}`);
|
||||||
} else {
|
} else {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
import { UserFollow } from '@components/user/follow';
|
import { UserFollow } from '@components/user/follow';
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
import { UserFollow } from '@components/user/follow';
|
import { UserFollow } from '@components/user/follow';
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
@ -8,7 +5,6 @@ import { DEFAULT_AVATAR } from '@stores/constants';
|
|||||||
import { shortenKey } from '@utils/shortenKey';
|
import { shortenKey } from '@utils/shortenKey';
|
||||||
|
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
import Image from 'next/image';
|
|
||||||
import { Author } from 'nostr-relaypool';
|
import { Author } from 'nostr-relaypool';
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
@ -27,21 +23,11 @@ export default function ProfileMetadata({ id }: { id: string }) {
|
|||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative h-56 w-full rounded-t-lg bg-zinc-800">
|
<div className="relative h-56 w-full rounded-t-lg bg-zinc-800">
|
||||||
<Image
|
<img src={profile?.banner || DEFAULT_BANNER} alt="user's banner" className="h-58 w-full object-cover" />
|
||||||
src={profile?.banner || DEFAULT_BANNER}
|
|
||||||
alt="user's banner"
|
|
||||||
fill={true}
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="relative -top-8 z-10 px-4">
|
<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">
|
<div className="relative h-16 w-16 rounded-lg bg-zinc-900 ring-2 ring-zinc-900">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture || DEFAULT_AVATAR} alt={id} className="h-16 w-16 rounded-lg object-cover" />
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={id}
|
|
||||||
fill={true}
|
|
||||||
className="rounded-lg object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { NoteBase } from '@components/note/base';
|
import { NoteBase } from '@components/note/base';
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DEFAULT_RELAYS } from '@stores/constants';
|
import { DEFAULT_RELAYS } from '@stores/constants';
|
||||||
|
|
||||||
import { RelayPool } from 'nostr-relaypool';
|
import { RelayPool } from 'nostr-relaypool';
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
@ -13,12 +11,7 @@ export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-full object-cover" />
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
fill={true}
|
|
||||||
className="rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
<span className="truncate font-medium leading-tight text-zinc-200">
|
<span className="truncate font-medium leading-tight text-zinc-200">
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
@ -16,12 +14,7 @@ export const UserExtend = ({ pubkey, time }: { pubkey: string; time: number }) =
|
|||||||
return (
|
return (
|
||||||
<div className="group flex h-11 items-center gap-2">
|
<div className="group flex h-11 items-center gap-2">
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-md object-cover" />
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
fill={true}
|
|
||||||
className="rounded-md object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
@ -11,12 +9,7 @@ export const UserFollow = ({ pubkey }: { pubkey: string }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-full object-cover" />
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
fill={true}
|
|
||||||
className="rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
<span className="truncate font-medium leading-tight text-zinc-200">
|
<span className="truncate font-medium leading-tight text-zinc-200">
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
@ -17,11 +15,10 @@ export const UserLarge = ({ pubkey, time }: { pubkey: string; time: number }) =>
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||||
<ImageWithFallback
|
<img
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
src={profile?.picture || DEFAULT_AVATAR}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
fill={true}
|
className="h-11 w-11 rounded-md border border-white/10 object-cover"
|
||||||
className="rounded-md border border-white/10 object-cover"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex-1">
|
<div className="w-full flex-1">
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
@ -11,12 +9,7 @@ export const UserMini = ({ pubkey }: { pubkey: string }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="group flex items-start gap-1">
|
<div className="group flex items-start gap-1">
|
||||||
<div className="relative h-7 w-7 shrink overflow-hidden rounded border border-white/10">
|
<div className="relative h-7 w-7 shrink overflow-hidden rounded border border-white/10">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-7 w-7 rounded object-cover" />
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
fill={true}
|
|
||||||
className="rounded object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium leading-none text-zinc-500">
|
<span className="text-xs font-medium leading-none text-zinc-500">
|
||||||
Replying to {profile?.name || shortenKey(pubkey)}
|
Replying to {profile?.name || shortenKey(pubkey)}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
@ -16,12 +14,7 @@ export const UserQuoteRepost = ({ pubkey, time }: { pubkey: string; time: number
|
|||||||
return (
|
return (
|
||||||
<div className="group flex items-center gap-2">
|
<div className="group flex items-center gap-2">
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||||
<ImageWithFallback
|
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-md object-cover" />
|
||||||
src={profile?.picture || DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
fill={true}
|
|
||||||
className="rounded-md object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-2 text-sm">
|
<div className="flex items-baseline gap-2 text-sm">
|
||||||
<h5 className="font-semibold leading-tight group-hover:underline">
|
<h5 className="font-semibold leading-tight group-hover:underline">
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ChannelMessages } from '@components/channels/messages';
|
import { ChannelMessages } from '@components/channels/messages';
|
||||||
import { FormChannel } from '@components/form/channel';
|
import { FormChannel } from '@components/form/channel';
|
||||||
|
import NewsfeedLayout from '@components/layouts/newsfeed';
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
import { channelMessagesAtom, channelReplyAtom } from '@stores/channel';
|
import { channelMessagesAtom, channelReplyAtom } from '@stores/channel';
|
||||||
import { FULL_RELAYS } from '@stores/constants';
|
import { FULL_RELAYS } from '@stores/constants';
|
||||||
|
|
||||||
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
||||||
|
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||||
|
|
||||||
import useLocalStorage from '@rehooks/local-storage';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useResetAtom } from 'jotai/utils';
|
import { useResetAtom } from 'jotai/utils';
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import { useContext, useEffect, useRef } from 'react';
|
import { useContext, useEffect, useRef } from 'react';
|
||||||
import useSWRSubscription from 'swr/subscription';
|
import useSWRSubscription from 'swr/subscription';
|
||||||
|
|
||||||
export default function Page() {
|
export function Page() {
|
||||||
const searchParams = useSearchParams();
|
const pageContext = usePageContext();
|
||||||
const id = searchParams.get('channel-id');
|
const searchParams: any = pageContext.urlParsed.search;
|
||||||
|
|
||||||
|
const id = searchParams.id;
|
||||||
|
|
||||||
const [pool]: any = useContext(RelayContext);
|
const [pool]: any = useContext(RelayContext);
|
||||||
const [activeAccount]: any = useLocalStorage('account', {});
|
const [activeAccount]: any = useLocalStorage('account', {});
|
||||||
@ -84,11 +85,13 @@ export default function Page() {
|
|||||||
}, [resetChannelReply, resetChannelMessages]);
|
}, [resetChannelReply, resetChannelMessages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-between">
|
<NewsfeedLayout>
|
||||||
<ChannelMessages />
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
<div className="shrink-0 p-3">
|
<ChannelMessages />
|
||||||
<FormChannel eventId={id} />
|
<div className="shrink-0 p-3">
|
||||||
|
<FormChannel eventId={id} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</NewsfeedLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,22 +1,24 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { MessageList } from '@components/chats/messageList';
|
import { MessageList } from '@components/chats/messageList';
|
||||||
import FormChat from '@components/form/chat';
|
import FormChat from '@components/form/chat';
|
||||||
|
import NewsfeedLayout from '@components/layouts/newsfeed';
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
import { chatMessagesAtom } from '@stores/chat';
|
import { chatMessagesAtom } from '@stores/chat';
|
||||||
import { FULL_RELAYS } from '@stores/constants';
|
import { FULL_RELAYS } from '@stores/constants';
|
||||||
|
|
||||||
|
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||||
|
|
||||||
import useLocalStorage from '@rehooks/local-storage';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useResetAtom } from 'jotai/utils';
|
import { useResetAtom } from 'jotai/utils';
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import useSWRSubscription from 'swr/subscription';
|
import useSWRSubscription from 'swr/subscription';
|
||||||
|
|
||||||
export default function Page() {
|
export function Page() {
|
||||||
const searchParams = useSearchParams();
|
const pageContext = usePageContext();
|
||||||
const pubkey = searchParams.get('pubkey');
|
const searchParams: any = pageContext.urlParsed.search;
|
||||||
|
|
||||||
|
const pubkey = searchParams.pubkey;
|
||||||
|
|
||||||
const [pool]: any = useContext(RelayContext);
|
const [pool]: any = useContext(RelayContext);
|
||||||
const [activeAccount]: any = useLocalStorage('account', {});
|
const [activeAccount]: any = useLocalStorage('account', {});
|
||||||
@ -62,11 +64,13 @@ export default function Page() {
|
|||||||
}, [resetChatMessages]);
|
}, [resetChatMessages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-between">
|
<NewsfeedLayout>
|
||||||
<MessageList />
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
<div className="shrink-0 p-3">
|
<MessageList />
|
||||||
<FormChat receiverPubkey={pubkey} />
|
<div className="shrink-0 p-3">
|
||||||
|
<FormChat receiverPubkey={pubkey} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</NewsfeedLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
||||||
@ -19,11 +17,10 @@ import { getParentID } from '@utils/transform';
|
|||||||
import LumeSymbol from '@assets/icons/Lume';
|
import LumeSymbol from '@assets/icons/Lume';
|
||||||
|
|
||||||
import { writeStorage } from '@rehooks/local-storage';
|
import { writeStorage } from '@rehooks/local-storage';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useCallback, useContext, useEffect, useRef } from 'react';
|
import { useCallback, useContext, useEffect, useRef } from 'react';
|
||||||
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
export default function Page() {
|
export function Page() {
|
||||||
const router = useRouter();
|
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
const now = useRef(new Date());
|
const now = useRef(new Date());
|
||||||
@ -126,7 +123,7 @@ export default function Page() {
|
|||||||
() => {
|
() => {
|
||||||
updateLastLogin(dateToUnix(now.current));
|
updateLastLogin(dateToUnix(now.current));
|
||||||
timeout.current = setTimeout(() => {
|
timeout.current = setTimeout(() => {
|
||||||
router.replace('/nostr/newsfeed/following', { forceOptimisticNavigation: true });
|
navigate('/newsfeed/following', { overwriteLastHistoryEntry: true });
|
||||||
}, 5000);
|
}, 5000);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -139,39 +136,41 @@ export default function Page() {
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[router, pool, relays]
|
[pool, relays]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let ignore = false;
|
let ignore = false;
|
||||||
|
|
||||||
getPlebs()
|
if (!ignore) {
|
||||||
.then((res) => {
|
getActiveAccount()
|
||||||
if (res && !ignore) {
|
.then((res: any) => {
|
||||||
writeStorage('plebs', res);
|
if (res) {
|
||||||
}
|
const account = res;
|
||||||
})
|
// update local storage
|
||||||
.catch(console.error);
|
writeStorage('account', account);
|
||||||
|
// fetch data
|
||||||
|
fetchData(account, account.follows);
|
||||||
|
} else {
|
||||||
|
navigate('/onboarding', { overwriteLastHistoryEntry: true });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
getActiveAccount()
|
getPlebs()
|
||||||
.then((res: any) => {
|
.then((res) => {
|
||||||
if (res && !ignore) {
|
if (res && !ignore) {
|
||||||
const account = res;
|
writeStorage('plebs', res);
|
||||||
// update local storage
|
}
|
||||||
writeStorage('account', account);
|
})
|
||||||
// fetch data
|
.catch(console.error);
|
||||||
fetchData(account, account.follows);
|
}
|
||||||
} else {
|
|
||||||
router.replace('/onboarding', { forceOptimisticNavigation: true });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ignore = true;
|
ignore = true;
|
||||||
clearTimeout(timeout.current);
|
clearTimeout(timeout.current);
|
||||||
};
|
};
|
||||||
}, [fetchData, router]);
|
}, [fetchData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
|
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
|
3
src/pages/newsfeed/circle/index.page.tsx
Normal file
3
src/pages/newsfeed/circle/index.page.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function Page() {
|
||||||
|
return <></>;
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import FormBase from '@components/form/base';
|
import FormBase from '@components/form/base';
|
||||||
|
import NewsfeedLayout from '@components/layouts/newsfeed';
|
||||||
import { NoteBase } from '@components/note/base';
|
import { NoteBase } from '@components/note/base';
|
||||||
import { Placeholder } from '@components/note/placeholder';
|
import { Placeholder } from '@components/note/placeholder';
|
||||||
import { NoteQuoteRepost } from '@components/note/quoteRepost';
|
import { NoteQuoteRepost } from '@components/note/quoteRepost';
|
||||||
@ -15,7 +14,7 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
export default function Page() {
|
export function Page() {
|
||||||
const [hasNewerNote, setHasNewerNote] = useAtom(hasNewerNoteAtom);
|
const [hasNewerNote, setHasNewerNote] = useAtom(hasNewerNoteAtom);
|
||||||
const setData = useSetAtom(notesAtom);
|
const setData = useSetAtom(notesAtom);
|
||||||
const data = useAtomValue(filteredNotesAtom);
|
const data = useAtomValue(filteredNotesAtom);
|
||||||
@ -82,29 +81,31 @@ export default function Page() {
|
|||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<NewsfeedLayout>
|
||||||
{hasNewerNote && (
|
<div className="relative h-full w-full">
|
||||||
<div className="absolute left-1/2 top-2 z-50 -translate-x-1/2 transform">
|
{hasNewerNote && (
|
||||||
<button
|
<div className="absolute left-1/2 top-2 z-50 -translate-x-1/2 transform">
|
||||||
onClick={() => loadLatest()}
|
<button
|
||||||
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"
|
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
|
<ArrowUp width={14} height={14} />
|
||||||
</button>
|
Load latest
|
||||||
</div>
|
</button>
|
||||||
)}
|
</div>
|
||||||
<Virtuoso
|
)}
|
||||||
ref={virtuosoRef}
|
<Virtuoso
|
||||||
data={data}
|
ref={virtuosoRef}
|
||||||
itemContent={itemContent}
|
data={data}
|
||||||
computeItemKey={computeItemKey}
|
itemContent={itemContent}
|
||||||
components={COMPONENTS}
|
computeItemKey={computeItemKey}
|
||||||
overscan={200}
|
components={COMPONENTS}
|
||||||
endReached={loadMore}
|
overscan={200}
|
||||||
className="scrollbar-hide h-full w-full overflow-y-auto"
|
endReached={loadMore}
|
||||||
/>
|
className="scrollbar-hide h-full w-full overflow-y-auto"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</NewsfeedLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
172
src/pages/onboarding/create/index.page.tsx
Normal file
172
src/pages/onboarding/create/index.page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,17 +1,17 @@
|
|||||||
'use client';
|
import OnboardingLayout from '@components/layouts/onboarding';
|
||||||
|
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
import { UserBase } from '@components/user/base';
|
import { UserBase } from '@components/user/base';
|
||||||
|
|
||||||
|
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||||
import { fetchProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
import { fetchProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||||
import { createPleb, updateAccount } from '@utils/storage';
|
import { createPleb, updateAccount } from '@utils/storage';
|
||||||
import { arrayToNIP02 } from '@utils/transform';
|
import { arrayToNIP02 } from '@utils/transform';
|
||||||
|
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import { CheckCircle } from 'iconoir-react';
|
import { CheckCircle } from 'iconoir-react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
import { Key, useCallback, useContext, useEffect, useState } from 'react';
|
import { Key, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
const supabase = createClient(
|
const supabase = createClient(
|
||||||
'https://niwaazauwnrwiwmnocnn.supabase.co',
|
'https://niwaazauwnrwiwmnocnn.supabase.co',
|
||||||
@ -53,12 +53,12 @@ const initialList = [
|
|||||||
{ pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609' },
|
{ pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Page() {
|
export function Page() {
|
||||||
const router = useRouter();
|
const pageContext = usePageContext();
|
||||||
const searchParams = useSearchParams();
|
const searchParams: any = pageContext.urlParsed.search;
|
||||||
|
|
||||||
const pubkey = searchParams.get('pubkey');
|
const pubkey = searchParams.pubkey;
|
||||||
const privkey = searchParams.get('privkey');
|
const privkey = searchParams.privkey;
|
||||||
|
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -100,8 +100,8 @@ export default function Page() {
|
|||||||
// broadcast
|
// broadcast
|
||||||
pool.publish(event, relays);
|
pool.publish(event, relays);
|
||||||
// redirect to splashscreen
|
// redirect to splashscreen
|
||||||
router.replace('/');
|
navigate('/');
|
||||||
}, [pubkey, privkey, follows, pool, relays, router]);
|
}, [pubkey, privkey, follows, pool, relays]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let ignore = false;
|
let ignore = false;
|
||||||
@ -122,65 +122,67 @@ export default function Page() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative grid h-full w-full grid-rows-5">
|
<OnboardingLayout>
|
||||||
<div className="row-span-1 flex items-center justify-center">
|
<div className="relative grid h-full w-full grid-rows-5">
|
||||||
<div className="text-center">
|
<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 leading-tight text-transparent">
|
<div className="text-center">
|
||||||
Personalized your newsfeed
|
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium leading-tight text-transparent">
|
||||||
</h1>
|
Personalized your newsfeed
|
||||||
<h3 className="text-lg text-zinc-500">
|
</h1>
|
||||||
Follow at least{' '}
|
<h3 className="text-lg text-zinc-500">
|
||||||
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
|
Follow at least{' '}
|
||||||
{follows.length}/10
|
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
|
||||||
</span>{' '}
|
{follows.length}/10
|
||||||
plebs
|
</span>{' '}
|
||||||
</h3>
|
plebs
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="row-span-4 h-full w-full overflow-y-auto">
|
||||||
<div className="row-span-4 h-full w-full overflow-y-auto">
|
<div className="grid grid-cols-4 gap-4 px-8 py-4">
|
||||||
<div className="grid grid-cols-4 gap-4 px-8 py-4">
|
{list.map((item: { pubkey: string }, index: Key) => (
|
||||||
{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
|
<button
|
||||||
key={index}
|
onClick={() => submit()}
|
||||||
onClick={() => toggleFollow(item.pubkey)}
|
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"
|
||||||
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} />
|
{loading === true ? (
|
||||||
{follows.includes(item.pubkey) && (
|
<svg
|
||||||
<div>
|
className="h-5 w-5 animate-spin text-zinc-900"
|
||||||
<CheckCircle width={16} height={16} className="text-zinc-400" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</div>
|
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>
|
</button>
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
{follows.length >= 10 && (
|
</OnboardingLayout>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
|
import OnboardingLayout from '@components/layouts/onboarding';
|
||||||
|
|
||||||
import { ArrowRight } from 'iconoir-react';
|
import { ArrowRight } from 'iconoir-react';
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
const PLEBS = [
|
const PLEBS = [
|
||||||
'https://133332.xyz/p.jpg',
|
'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 (
|
return (
|
||||||
<div className="grid h-full w-full grid-rows-5">
|
<OnboardingLayout>
|
||||||
<div className="row-span-3 overflow-hidden">
|
<div className="grid h-full w-full grid-rows-5">
|
||||||
<div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4">
|
<div className="row-span-3 overflow-hidden">
|
||||||
{[...new Array(ROWS)].map((_, i) => (
|
<div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4">
|
||||||
<InfiniteLoopSlider key={i} duration={random(DURATION - 5000, DURATION + 20000)} reverse={i % 2}>
|
{[...new Array(ROWS)].map((_, i) => (
|
||||||
{shuffle(PLEBS)
|
<InfiniteLoopSlider key={i} duration={random(DURATION - 5000, DURATION + 20000)} reverse={i % 2}>
|
||||||
.slice(0, PLEBS_PER_ROW)
|
{shuffle(PLEBS)
|
||||||
.map((tag) => (
|
.slice(0, PLEBS_PER_ROW)
|
||||||
<div
|
.map((tag) => (
|
||||||
key={tag}
|
<div key={tag} className="relative mr-4 h-11 w-11 gap-2 rounded-md bg-zinc-900 shadow-xl">
|
||||||
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"
|
<img src={tag} alt={tag} className="h-11 w-11 rounded-md border border-zinc-900" />
|
||||||
>
|
</div>
|
||||||
<Image
|
))}
|
||||||
src={tag}
|
</InfiniteLoopSlider>
|
||||||
alt={tag}
|
))}
|
||||||
fill={true}
|
<div className="pointer-events-none absolute inset-0 bg-fade" />
|
||||||
className="rounded-md border border-zinc-900"
|
</div>
|
||||||
placeholder="blur"
|
</div>
|
||||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
|
<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">
|
||||||
priority
|
<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's start!
|
||||||
</div>
|
</h1>
|
||||||
))}
|
<div className="mt-4 flex flex-col items-center gap-1.5">
|
||||||
</InfiniteLoopSlider>
|
<a
|
||||||
))}
|
href="/onboarding/create"
|
||||||
<div className="pointer-events-none absolute inset-0 bg-fade" />
|
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>
|
</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">
|
</OnboardingLayout>
|
||||||
<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'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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
121
src/pages/onboarding/login/index.page.tsx
Normal file
121
src/pages/onboarding/login/index.page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
144
src/pages/onboarding/login/step-2/index.page.tsx
Normal file
144
src/pages/onboarding/login/step-2/index.page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
57
src/pages/user/index.page.tsx
Normal file
57
src/pages/user/index.page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
20
src/renderer/_default.page.client.tsx
Normal file
20
src/renderer/_default.page.client.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
30
src/renderer/_default.page.server.tsx
Normal file
30
src/renderer/_default.page.server.tsx
Normal 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>`;
|
||||||
|
}
|
17
src/renderer/_error.page.tsx
Normal file
17
src/renderer/_error.page.tsx
Normal 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
17
src/renderer/shell.tsx
Normal 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
35
src/renderer/types.ts
Normal 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;
|
22
src/utils/hooks/usePageContext.tsx
Normal file
22
src/utils/hooks/usePageContext.tsx
Normal 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;
|
||||||
|
}
|
@ -6,7 +6,8 @@
|
|||||||
"@components/*": ["src/components/*"],
|
"@components/*": ["src/components/*"],
|
||||||
"@stores/*": ["src/stores/*"],
|
"@stores/*": ["src/stores/*"],
|
||||||
"@utils/*": ["src/utils/*"],
|
"@utils/*": ["src/utils/*"],
|
||||||
"@assets/*": ["src/assets/*"]
|
"@assets/*": ["src/assets/*"],
|
||||||
|
"@renderer/*": ["src/renderer/*"]
|
||||||
},
|
},
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
@ -4,5 +4,5 @@ import ssr from 'vite-plugin-ssr/plugin';
|
|||||||
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), ssr(), viteTsconfigPaths()],
|
plugins: [react(), ssr({ prerender: true }), viteTsconfigPaths()],
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user