updated onboarding process use new storage util

This commit is contained in:
Ren Amamiya
2023-03-23 15:48:57 +07:00
parent 705430a11e
commit 18a9bf3e49
6 changed files with 196 additions and 212 deletions

View File

@@ -1,13 +1,13 @@
import { DatabaseContext } from '@components/contexts/database';
import { ImageWithFallback } from '@components/imageWithFallback'; import { ImageWithFallback } from '@components/imageWithFallback';
import { createCacheProfile } from '@utils/storage';
import { truncate } from '@utils/truncate'; import { truncate } from '@utils/truncate';
import { fetch } from '@tauri-apps/api/http'; import { fetch } from '@tauri-apps/api/http';
import { memo, useCallback, useContext, useEffect, useState } from 'react'; import destr from 'destr';
import { memo, useCallback, useEffect, useState } from 'react';
export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) { export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
const { db }: any = useContext(DatabaseContext);
const [profile, setProfile] = useState(null); const [profile, setProfile] = useState(null);
const fetchProfile = useCallback(async (id: string) => { const fetchProfile = useCallback(async (id: string) => {
@@ -15,24 +15,17 @@ export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
method: 'GET', method: 'GET',
timeout: 30, timeout: 30,
}); });
return res; return res.data;
}, []); }, []);
const cacheProfile = useCallback(
async (event) => {
// insert to database
await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [pubkey, event.content]);
// update state
setProfile(JSON.parse(event.content));
},
[db, pubkey]
);
useEffect(() => { useEffect(() => {
fetchProfile(pubkey) fetchProfile(pubkey)
.then((res) => cacheProfile(res)) .then((res: any) => {
setProfile(destr(res.content));
createCacheProfile(res.pubkey, res.content);
})
.catch(console.error); .catch(console.error);
}, [fetchProfile, cacheProfile, pubkey]); }, [fetchProfile, pubkey]);
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -41,7 +34,7 @@ export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" /> <ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
)} )}
</div> </div>
<div className="flex w-full flex-1 flex-col items-start"> <div className="flex w-full flex-1 flex-col items-start text-start">
<span className="font-medium leading-tight text-zinc-200">{profile?.display_name || profile?.name}</span> <span className="font-medium leading-tight text-zinc-200">{profile?.display_name || profile?.name}</span>
<span className="text-sm leading-tight text-zinc-400">{truncate(pubkey, 16, ' .... ')}</span> <span className="text-sm leading-tight text-zinc-400">{truncate(pubkey, 16, ' .... ')}</span>
</div> </div>

View File

@@ -1,14 +1,13 @@
import BaseLayout from '@layouts/base'; import BaseLayout from '@layouts/base';
import { DatabaseContext } from '@components/contexts/database'; import { pool } from '@utils/pool';
import { RelayContext } from '@components/contexts/relay'; import { createAccount, getAllRelays } from '@utils/storage';
import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons'; import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
import { useLocalStorage, writeStorage } from '@rehooks/local-storage';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools'; import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useMemo, useState } from 'react'; import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useMemo, useState } from 'react';
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator'; import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
const config: Config = { const config: Config = {
@@ -18,11 +17,6 @@ const config: Config = {
export default function Page() { export default function Page() {
const router = useRouter(); const router = useRouter();
const { db }: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const [relays] = useLocalStorage('relays');
const [type, setType] = useState('password'); const [type, setType] = useState('password');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -33,16 +27,8 @@ export default function Page() {
const npub = nip19.npubEncode(pubKey); const npub = nip19.npubEncode(pubKey);
const nsec = nip19.nsecEncode(privKey); const nsec = nip19.nsecEncode(privKey);
// toggle privatek key // auto-generated profile metadata
const showPrivateKey = () => { const metadata = useMemo(
if (type === 'password') {
setType('text');
} else {
setType('password');
}
};
// auto-generated profile
const data = useMemo(
() => ({ () => ({
display_name: name, display_name: name,
name: name, name: name,
@@ -52,23 +38,35 @@ export default function Page() {
}), }),
[name] [name]
); );
// insert to database
const insertDB = async () => { // build profile
await db.execute('INSERT OR IGNORE INTO accounts (id, privkey, npub, nsec, metadata) VALUES (?, ?, ?, ?, ?)', [ const data = useMemo(
pubKey, () => ({
privKey, pubkey: pubKey,
npub, privkey: privKey,
nsec, npub: npub,
data, nsec: nsec,
]); metadata: metadata,
}),
[metadata, npub, nsec, privKey, pubKey]
);
// toggle privatek key
const showPrivateKey = () => {
if (type === 'password') {
setType('text');
} else {
setType('password');
}
}; };
// build event and broadcast to all relays
const createAccount = () => { // create account and broadcast to all relays
const submit = () => {
setLoading(true); setLoading(true);
// build event // build event
const event: any = { const event: any = {
content: JSON.stringify(data), content: JSON.stringify(metadata),
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
kind: 0, kind: 0,
pubkey: pubKey, pubkey: pubKey,
@@ -76,23 +74,20 @@ export default function Page() {
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, privKey); event.sig = signEvent(event, privKey);
// insert to database then broadcast // insert to database then broadcast
insertDB() createAccount(data)
.then(() => { .then(() => {
// publish to relays getAllRelays()
relayPool.publish(event, relays); .then((res) => {
// set currentUser in global state // publish to relays
writeStorage('current-user', { pool(res).publish(event, res);
metadata: JSON.stringify(data), router.push({
npub: npub, pathname: '/onboarding/create/step-2',
privkey: privKey, query: { id: pubKey, privkey: privKey },
id: pubKey, });
}); })
// redirect to pre-follow .catch(console.error);
setTimeout(() => {
setLoading(false);
router.push('/onboarding/create/step-2');
}, 1500);
}) })
.catch(console.error); .catch(console.error);
}; };
@@ -146,12 +141,12 @@ export default function Page() {
<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="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-4"> <div className="flex space-x-4">
<div className="relative h-10 w-10 rounded-full"> <div className="relative h-10 w-10 rounded-full">
<Image className="inline-block rounded-full" src={data.picture} alt="" fill={true} /> <Image className="inline-block rounded-full" src={metadata.picture} alt="" fill={true} />
</div> </div>
<div className="flex-1 space-y-4 py-1"> <div className="flex-1 space-y-4 py-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="font-semibold">{data.display_name}</p> <p className="font-semibold">{metadata.display_name}</p>
<p className="text-zinc-400">@{data.username}</p> <p className="text-zinc-400">@{metadata.username}</p>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
@@ -183,7 +178,7 @@ export default function Page() {
</svg> </svg>
) : ( ) : (
<button <button
onClick={() => createAccount()} 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" 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> <span className="drop-shadow-lg">Continue </span>

View File

@@ -1,24 +1,15 @@
import BaseLayout from '@layouts/base'; import BaseLayout from '@layouts/base';
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
import { UserBase } from '@components/user/base'; import { UserBase } from '@components/user/base';
import { pool } from '@utils/pool';
import { createFollows, getAllRelays } from '@utils/storage';
import { CheckCircledIcon } from '@radix-ui/react-icons'; import { CheckCircledIcon } from '@radix-ui/react-icons';
import useLocalStorage from '@rehooks/local-storage';
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getEventHash, nip19, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from 'nostr-tools';
import { import { JSXElementConstructor, Key, ReactElement, ReactFragment, ReactPortal, useEffect, useState } from 'react';
JSXElementConstructor,
Key,
ReactElement,
ReactFragment,
ReactPortal,
useContext,
useEffect,
useState,
} from 'react';
const supabase = createClient( const supabase = createClient(
'https://niwaazauwnrwiwmnocnn.supabase.co', 'https://niwaazauwnrwiwmnocnn.supabase.co',
@@ -62,12 +53,7 @@ const initialList = [
export default function Page() { export default function Page() {
const router = useRouter(); const router = useRouter();
const { id, privkey }: any = router.query;
const { db }: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const [currentUser]: any = useLocalStorage('current-user');
const [relays] = useLocalStorage('relays');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [list, setList]: any = useState(initialList); const [list, setList]: any = useState(initialList);
@@ -78,31 +64,19 @@ export default function Page() {
const arr = follows.includes(pubkey) ? follows.filter((i) => i !== pubkey) : [...follows, pubkey]; const arr = follows.includes(pubkey) ? follows.filter((i) => i !== pubkey) : [...follows, pubkey];
setFollows(arr); setFollows(arr);
}; };
// insert follow to database
const insertDB = async () => {
// self follow
await db.execute(
`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${currentUser.id}", "${currentUser.id}", "0")`
);
// follow selected
follows.forEach(async (pubkey) => {
await db.execute(
`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${pubkey}", "${currentUser.id}", "0")`
);
});
};
// build event tags // build event tags
const createTags = () => { const tags = () => {
const tags = []; const arr = [];
// push item to tags // push item to tags
follows.forEach((item) => { follows.forEach((item) => {
tags.push(['p', item]); arr.push(['p', item]);
}); });
return arr;
return tags;
}; };
// commit and publish to relays
const createFollows = () => { // save follows to database then broadcast
const submit = () => {
setLoading(true); setLoading(true);
// build event // build event
@@ -110,21 +84,25 @@ export default function Page() {
content: '', content: '',
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
kind: 3, kind: 3,
pubkey: currentUser.id, pubkey: id,
tags: createTags(), tags: tags(),
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, currentUser.privkey); event.sig = signEvent(event, privkey);
insertDB().then(() => { createFollows(follows, id, 0)
// publish to relays .then((res) => {
relayPool.publish(event, relays); if (res === 'ok') {
// redirect to home getAllRelays()
setTimeout(() => { .then((res) => {
setLoading(false); // publish to relays
router.push('/'); pool(res).publish(event, res);
}, 1000); router.push('/');
}); })
.catch(console.error);
}
})
.catch(console.error);
}; };
useEffect(() => { useEffect(() => {
@@ -174,7 +152,7 @@ export default function Page() {
{follows.length >= 10 && ( {follows.length >= 10 && (
<div className="fixed bottom-0 left-0 z-10 flex h-24 w-full items-center justify-center"> <div className="fixed bottom-0 left-0 z-10 flex h-24 w-full items-center justify-center">
<button <button
onClick={() => createFollows()} 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" 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 ? ( {loading === true ? (

View File

@@ -1,105 +1,75 @@
import BaseLayout from '@layouts/base'; import BaseLayout from '@layouts/base';
import { DatabaseContext } from '@components/contexts/database'; import { pool } from '@utils/pool';
import { RelayContext } from '@components/contexts/relay'; import { createAccount, createFollows, getAllRelays } from '@utils/storage';
import { truncate } from '@utils/truncate';
import { useLocalStorage, writeStorage } from '@rehooks/local-storage'; import destr from 'destr';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getPublicKey, nip19 } from 'nostr-tools'; import { getPublicKey, nip19 } from 'nostr-tools';
import { import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useEffect, useState } from 'react';
JSXElementConstructor,
ReactElement, const tags = (arr) => {
ReactFragment, const newarr = [];
ReactPortal, // push item to newarr
useCallback, arr.forEach((item) => {
useContext, newarr.push(['p', item]);
useEffect, });
useMemo, return newarr;
useState, };
} from 'react';
export default function Page() { export default function Page() {
const { db }: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const router = useRouter(); const router = useRouter();
const { privkey }: any = router.query; const privkey: any = router.query.privkey;
const pubkey = getPublicKey(privkey);
const [relays] = useLocalStorage('relays'); const [profile, setProfile] = useState(null);
const [profile, setProfile] = useState({ picture: '', display_name: '', username: '' });
const pubkey = useMemo(() => (privkey ? getPublicKey(privkey) : null), [privkey]); useEffect(() => {
getAllRelays()
.then((res) => {
pool(res).subscribe(
[
{
authors: [pubkey],
kinds: [0, 3],
since: 0,
},
],
res,
(event: any) => {
if (event.kind === 0) {
const data = {
pubkey: pubkey,
privkey: privkey,
npub: nip19.npubEncode(pubkey),
nsec: nip19.nsecEncode(privkey),
metadata: event.content,
};
setProfile(destr(event.content));
createAccount(data);
} else {
if (event.tags.length > 0) {
createFollows(tags(event.tags), pubkey, 0);
}
}
},
undefined,
undefined,
{
unsubscribeOnEose: true,
}
);
})
.catch(console.error);
}, [privkey, pubkey]);
// save account to database
const insertAccount = useCallback(
async (metadata) => {
const npub = privkey ? nip19.npubEncode(pubkey) : null;
const nsec = privkey ? nip19.nsecEncode(privkey) : null;
// insert to database
await db.execute('INSERT OR IGNORE INTO accounts (id, privkey, npub, nsec, metadata) VALUES (?, ?, ?, ?, ?)', [
pubkey,
privkey,
npub,
nsec,
metadata,
]);
// write to localstorage
writeStorage('current-user', { id: pubkey, privkey: privkey, npub: npub, nsec: nsec, metadata: metadata });
// update state
setProfile(JSON.parse(metadata));
},
[db, privkey, pubkey]
);
// save follows to database
const insertFollows = useCallback(
async (follows) => {
follows.forEach(async (item) => {
if (item) {
// insert to database
await db.execute(
`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${item[1]}", "${pubkey}", "0")`
);
}
});
},
[db, pubkey]
);
// submit then redirect to home // submit then redirect to home
const submit = () => { const submit = () => {
router.push('/'); router.push('/');
}; };
useEffect(() => {
const unsubscribe = relayPool.subscribe(
[
{
authors: [pubkey],
kinds: [0, 3],
since: 0,
},
],
relays,
(event: any) => {
if (event.kind === 0) {
insertAccount(event.content);
} else {
if (event.tags.length > 0) {
insertFollows(event.tags);
}
}
},
undefined,
(events: any, relayURL: any) => {
console.log(events, relayURL);
}
);
return () => {
unsubscribe();
};
}, [insertAccount, insertFollows, pubkey, relayPool, relays]);
return ( return (
<div className="grid h-full w-full grid-rows-5"> <div className="grid h-full w-full grid-rows-5">
<div className="row-span-1 flex items-center justify-center"> <div className="row-span-1 flex items-center justify-center">
@@ -115,13 +85,13 @@ export default function Page() {
<div className="w-full rounded-lg bg-zinc-900 p-4 shadow-input ring-1 ring-zinc-800"> <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="flex space-x-4">
<div className="relative h-10 w-10 rounded-full"> <div className="relative h-10 w-10 rounded-full">
<Image className="inline-block rounded-full" src={profile.picture} alt="" fill={true} /> <Image className="inline-block rounded-full" src={profile?.picture} alt="" fill={true} />
</div> </div>
<div className="flex-1 space-y-4 py-1"> <div className="flex-1 space-y-4 py-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="font-semibold">{profile.display_name}</p> <p className="font-semibold">{profile?.display_name || profile?.name}</p>
<span className="leading-tight text-zinc-500">·</span> <span className="leading-tight text-zinc-500">·</span>
<p className="text-zinc-500">@{profile.username}</p> <p className="text-zinc-500">@{profile?.username || truncate(pubkey, 16, ' .... ')}</p>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">

6
src/utils/pool.tsx Normal file
View File

@@ -0,0 +1,6 @@
import { RelayPool } from 'nostr-relaypool';
export function pool({ relays }: { relays: any }) {
const createPool = new RelayPool(relays, { useEventCache: false, logSubscriptions: false });
return createPool;
}

View File

@@ -15,17 +15,59 @@ export async function connect(): Promise<Database> {
// get all relays // get all relays
export async function getAllRelays() { export async function getAllRelays() {
const db = await connect(); const db = await connect();
return await db.select('SELECT relay_url FROM relays WHERE relay_status = "1"'); const result: any = await db.select('SELECT relay_url FROM relays WHERE relay_status = "1";');
return result.reduce((relays, { relay_url }) => {
relays.push(relay_url);
return relays;
}, []);
} }
// get active account // get active account
export async function getActiveAccount() { export async function getActiveAccount() {
const db = await connect(); const db = await connect();
return await db.select(`SELECT * FROM accounts LIMIT 1`); return await db.select(`SELECT * FROM accounts LIMIT 1;`);
} }
// get all follows by account id // get all follows by account id
export async function getAllFollowsByID(id: string) { export async function getAllFollowsByID(id) {
const db = await connect(); const db = await connect();
return await db.select(`SELECT pubkey FROM follows WHERE account = "${id}"`); return await db.select(`SELECT pubkey FROM follows WHERE account = "${id}";`);
}
// create account
export async function createAccount(data) {
const db = await connect();
return await db.execute(
'INSERT OR IGNORE INTO accounts (id, privkey, npub, nsec, metadata) VALUES (?, ?, ?, ?, ?);',
[data.pubkey, data.privkey, data.npub, data.nsec, data.metadata]
);
}
// create follow
export async function createFollow(pubkey, account, kind) {
const db = await connect();
return await db.execute('INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES (?, ?, ?);', [
pubkey,
account,
kind || 0,
]);
}
// create follow
export async function createFollows(data, account, kind) {
const db = await connect();
data.forEach(async (item) => {
await db.execute('INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES (?, ?, ?);', [
item,
account,
kind || 0,
]);
});
return 'ok';
}
// create cache profile
export async function createCacheProfile(id, metadata) {
const db = await connect();
return await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [id, metadata]);
} }