refactor initial database and state management

This commit is contained in:
Ren Amamiya 2023-03-01 15:35:10 +07:00
parent 964343ccc8
commit 458f826958
24 changed files with 259 additions and 506 deletions

View File

@ -8,16 +8,7 @@
"endOfLine": "lf",
"bracketSpacing": true,
"bracketSameLine": true,
"importOrder": [
"^@layouts/(.*)$",
"^@pages/(.*)$",
"^@components/(.*)$",
"^@utils/(.*)$",
"^@stores/(.*)$",
"^@assets/(.*)$",
"<THIRD_PARTY_MODULES>",
"^[./]"
],
"importOrder": ["^@layouts/(.*)$", "^@pages/(.*)$", "^@components/(.*)$", "^@utils/(.*)$", "^@stores/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],

View File

@ -12,11 +12,10 @@
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@nanostores/persistent": "^0.7.0",
"@nanostores/react": "^0.4.1",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.3",
"@radix-ui/react-icons": "^1.2.0",
"@rehooks/local-storage": "^2.4.4",
"@tauri-apps/api": "^1.2.0",
"@uiw/react-markdown-preview": "^4.1.9",
"@uiw/react-md-editor": "^3.20.5",

View File

@ -1,11 +1,10 @@
lockfileVersion: 5.4
specifiers:
'@nanostores/persistent': ^0.7.0
'@nanostores/react': ^0.4.1
'@radix-ui/react-dialog': ^1.0.2
'@radix-ui/react-dropdown-menu': ^2.0.3
'@radix-ui/react-icons': ^1.2.0
'@rehooks/local-storage': ^2.4.4
'@tailwindcss/typography': ^0.5.9
'@tauri-apps/api': ^1.2.0
'@tauri-apps/cli': ^1.2.3
@ -54,11 +53,10 @@ specifiers:
ws: ^8.12.1
dependencies:
'@nanostores/persistent': 0.7.0_nanostores@0.7.4
'@nanostores/react': 0.4.1_nkfnbc2tpc77iht7asm3uqwau4
'@radix-ui/react-dialog': 1.0.2_zula6vjvt3wdocc4mwcxqa6nzi
'@radix-ui/react-dropdown-menu': 2.0.3_zula6vjvt3wdocc4mwcxqa6nzi
'@radix-ui/react-icons': 1.2.0_react@18.2.0
'@rehooks/local-storage': 2.4.4_react@18.2.0
'@tauri-apps/api': 1.2.0
'@uiw/react-markdown-preview': 4.1.9_zula6vjvt3wdocc4mwcxqa6nzi
'@uiw/react-md-editor': 3.20.5_zula6vjvt3wdocc4mwcxqa6nzi
@ -467,27 +465,6 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: false
/@nanostores/persistent/0.7.0_nanostores@0.7.4:
resolution: { integrity: sha512-4PAInL/T1hbftZUJ0cmgdFHBMalUoq7BUXFBy7QfyMv/8X3LPTYNh/yxspL7+J+XM3UNvVI7IFRMMs6FBasjhQ== }
engines: { node: ^14.0.0 || ^16.0.0 || >=18.0.0 }
peerDependencies:
nanostores: ^0.7.0
dependencies:
nanostores: 0.7.4
dev: false
/@nanostores/react/0.4.1_nkfnbc2tpc77iht7asm3uqwau4:
resolution: { integrity: sha512-lsv0CYrMxczbXtoV/mxFVEoL/uVjEjseoP89srO/5yNAOkJka+dSFS7LYyWEbuvCPO7EgbtkvRpO5V+OztKQOw== }
engines: { node: ^14.0.0 || ^16.0.0 || >=18.0.0 }
peerDependencies:
nanostores: ^0.7.0
react: '>=18.0.0'
dependencies:
nanostores: 0.7.4
react: 18.2.0
use-sync-external-store: 1.2.0_react@18.2.0
dev: false
/@next/env/13.2.1:
resolution: { integrity: sha512-Hq+6QZ6kgmloCg8Kgrix+4F0HtvLqVK3FZAnlAoS0eonaDemHe1Km4kwjSWRE3JNpJNcKxFHF+jsZrYo0SxWoQ== }
dev: false
@ -1009,6 +986,14 @@ packages:
'@babel/runtime': 7.21.0
dev: false
/@rehooks/local-storage/2.4.4_react@18.2.0:
resolution: { integrity: sha512-zE+kfOkG59n/1UTxdmbwktIosclr67Nlbf2MzUJ9mNtCSypVscNHeD1qT6JCSo5Pjj8DO893IKWNLJqKKzDL/Q== }
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.2.0
dev: false
/@rushstack/eslint-patch/1.2.0:
resolution: { integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== }
dev: true
@ -5374,14 +5359,6 @@ packages:
tslib: 2.5.0
dev: false
/use-sync-external-store/1.2.0_react@18.2.0:
resolution: { integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== }
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/util-deprecate/1.0.2:
resolution: { integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== }
dev: true

View File

@ -1,4 +1,28 @@
-- Add migration script here
-- create relays
CREATE TABLE
relays (
id INTEGER PRIMARY KEY,
relay_url TEXT NOT NULL,
relay_status INTEGER NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO
relays (relay_url, relay_status)
VALUES
("wss://relay.damus.io", "1"),
("wss://relay.uselume.xyz", "0"),
("wss://nostr-pub.wellorder.net", "1"),
("wss://nostr.bongbong.com", "1"),
("wss://nostr.zebedee.cloud", "1"),
("wss://nostr.fmt.wiz.biz", "1"),
("wss://nostr.walletofsatoshi.com", "1"),
("wss://relay.snort.social", "1"),
("wss://offchain.pub", "1"),
("wss://nos.lol", "1");
-- create accounts
CREATE TABLE
accounts (
@ -6,6 +30,7 @@ CREATE TABLE
privkey TEXT NOT NULL,
npub TEXT NOT NULL,
nsec TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 0,
metadata JSON
);
@ -40,5 +65,6 @@ CREATE TABLE
kind INTEGER NOT NULL DEFAULT 1,
tags TEXT NOT NULL,
content TEXT NOT NULL,
relay TEXT,
is_multi BOOLEAN DEFAULT 0
);

View File

@ -1,19 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Account } from '@components/accountBar/account';
import { currentUser } from '@stores/currentUser';
import LumeSymbol from '@assets/icons/Lume';
import { useStore } from '@nanostores/react';
import { PlusIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
export default function AccountBar() {
const [users, setUsers] = useState([]);
const $currentUser: any = useStore(currentUser);
const [currentUser]: any = useLocalStorage('current-user');
const getAccounts = useCallback(async () => {
const db = await Database.load('sqlite:lume.db');
@ -30,7 +27,7 @@ export default function AccountBar() {
<div className="flex h-full flex-col items-center justify-between px-2 pt-12 pb-4">
<div className="flex flex-col gap-4">
{users.map((user, index) => (
<Account key={index} user={user} current={$currentUser.pubkey} />
<Account key={index} user={user} current={currentUser.pubkey} />
))}
<Link
href="/onboarding"

View File

@ -4,10 +4,7 @@ import { RelayContext } from '@components/contexts/relay';
import { dateToUnix, hoursAgo } from '@utils/getDate';
import { follows } from '@stores/follows';
import { relays } from '@stores/relays';
import { useStore } from '@nanostores/react';
import { useLocalStorage } from '@rehooks/local-storage';
import { memo, useCallback, useContext, useRef } from 'react';
export const NoteConnector = memo(function NoteConnector() {
@ -16,8 +13,8 @@ export const NoteConnector = memo(function NoteConnector() {
const now = useRef(new Date());
const $follows = useStore(follows);
const $relays = useStore(relays);
const [follows]: any = useLocalStorage('follows');
const [relays]: any = useLocalStorage('relays');
const insertDB = useCallback(
async (event: any) => {
@ -35,11 +32,11 @@ export const NoteConnector = memo(function NoteConnector() {
[
{
kinds: [1],
authors: $follows,
authors: follows,
since: dateToUnix(hoursAgo(12, now.current)),
},
],
$relays,
relays,
(event: any) => {
insertDB(event).catch(console.error);
},

View File

@ -1,13 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createContext } from 'react';
import { writeStorage } from '@rehooks/local-storage';
import { createContext, useEffect, useMemo } from 'react';
import Database from 'tauri-plugin-sql-api';
export const DatabaseContext = createContext({});
const db = typeof window !== 'undefined' ? await Database.load('sqlite:lume.db') : null;
const initDB = typeof window !== 'undefined' ? await Database.load('sqlite:lume.db') : null;
export default function DatabaseProvider({ children }: { children: React.ReactNode }) {
const value = db;
const db = useMemo(() => initDB, []);
return <DatabaseContext.Provider value={value}>{children}</DatabaseContext.Provider>;
useEffect(() => {
const getRelays = async () => {
const arr = [];
const result: any[] = await db.select('SELECT relay_url FROM relays WHERE relay_status = "1"');
result.forEach((item: { relay_url: string }) => {
arr.push(item.relay_url);
});
writeStorage('relays', arr);
};
const getAccount = async () => {
const result = await db.select(`SELECT * FROM accounts LIMIT 1`);
writeStorage('current-user', result[0]);
return result[0];
};
const getFollows = async (id: string) => {
const arr = [];
const result: any[] = await db.select(`SELECT pubkey FROM follows WHERE account = "${id}"`);
result.forEach((item: { pubkey: string }) => {
arr.push(item.pubkey);
});
writeStorage('follows', arr);
};
if (db !== null) {
getRelays().catch(console.error);
getAccount()
.then((res) => {
if (res) {
getFollows(res.id).catch(console.error);
}
})
.catch(console.error);
}
}, [db]);
return <DatabaseContext.Provider value={{ db }}>{children}</DatabaseContext.Provider>;
}

View File

@ -1,8 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { currentUser } from '@stores/currentUser';
import { useStore } from '@nanostores/react';
import * as Dialog from '@radix-ui/react-dialog';
import { useLocalStorage } from '@rehooks/local-storage';
import * as commands from '@uiw/react-md-editor/lib/commands';
import dynamic from 'next/dynamic';
import { dateToUnix, useNostr } from 'nostr-react';
@ -17,9 +15,9 @@ export default function CreatePost() {
const { publish } = useNostr();
const [value, setValue] = useState('');
const $currentUser: any = useStore(currentUser);
const pubkey = $currentUser.pubkey;
const privkey = $currentUser.privkey;
const [currentUser]: any = useLocalStorage('current-user');
const pubkey = currentUser.pubkey;
const privkey = currentUser.privkey;
const postButton = {
name: 'post',
@ -27,9 +25,7 @@ export default function CreatePost() {
buttonProps: { className: 'cta-btn', 'aria-label': 'Post a message' },
icon: (
<div className="relative inline-flex h-10 w-16 transform cursor-pointer overflow-hidden rounded bg-zinc-900 px-2.5 ring-zinc-500/50 ring-offset-zinc-900 will-change-transform focus:outline-none focus:ring-1 focus:ring-offset-2 active:translate-y-1">
<span className="absolute inset-px z-10 inline-flex items-center justify-center rounded bg-zinc-900 text-zinc-200">
Post
</span>
<span className="absolute inset-px z-10 inline-flex items-center justify-center rounded bg-zinc-900 text-zinc-200">Post</span>
<span className="absolute inset-0 z-0 scale-x-[2.0] blur before:absolute before:inset-0 before:top-1/2 before:aspect-square before:animate-disco before:bg-gradient-conic before:from-gray-300 before:via-fuchsia-600 before:to-orange-600"></span>
</div>
),

View File

@ -4,14 +4,12 @@ import { NoteConnector } from '@components/connectors/note';
import CreatePost from '@components/navigatorBar/createPost';
import { ProfileMenu } from '@components/navigatorBar/profileMenu';
import { currentUser } from '@stores/currentUser';
import { useStore } from '@nanostores/react';
import { PlusIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
export default function NavigatorBar() {
const $currentUser: any = useStore(currentUser);
const profile = $currentUser.metadata !== undefined ? JSON.parse($currentUser.metadata) : { display_name: null, username: null };
const [currentUser]: any = useLocalStorage('current-user');
const profile = currentUser.metadata !== undefined ? JSON.parse(currentUser.metadata) : { display_name: null, username: null };
return (
<div className="flex h-full flex-col flex-wrap justify-between overflow-hidden px-2 pt-3 pb-4">
@ -25,7 +23,7 @@ export default function NavigatorBar() {
<div className="flex flex-col p-2">
<div className="flex items-center justify-between">
<h5 className="font-semibold leading-tight text-zinc-100">{profile.display_name || ''}</h5>
<ProfileMenu pubkey={$currentUser.pubkey} />
<ProfileMenu pubkey={currentUser.pubkey} />
</div>
<span className="text-sm leading-tight text-zinc-500">@{profile.username || ''}</span>
</div>

View File

@ -1,26 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { currentUser } from '@stores/currentUser';
import { useStore } from '@nanostores/react';
import { HeartFilledIcon, HeartIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
import { dateToUnix, useNostr, useNostrEvents } from 'nostr-react';
import { getEventHash, signEvent } from 'nostr-tools';
import { useState } from 'react';
export default function Reaction({
eventID,
eventPubkey,
}: {
eventID: string;
eventPubkey: string;
}) {
export default function Reaction({ eventID, eventPubkey }: { eventID: string; eventPubkey: string }) {
const { publish } = useNostr();
const [reaction, setReaction] = useState(0);
const [isReact, setIsReact] = useState(false);
const $currentUser: any = useStore(currentUser);
const pubkey = $currentUser.pubkey;
const privkey = $currentUser.privkey;
const [currentUser]: any = useLocalStorage('current-user');
const pubkey = currentUser.pubkey;
const privkey = currentUser.privkey;
const { onEvent } = useNostrEvents({
filter: {
@ -65,15 +57,9 @@ export default function Reaction({
};
return (
<button
onClick={(e) => handleReaction(e)}
className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
<button onClick={(e) => handleReaction(e)} className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
<div className="rounded-lg p-1 group-hover:bg-zinc-600">
{isReact ? (
<HeartFilledIcon className="h-4 w-4 group-hover:text-red-400" />
) : (
<HeartIcon className="h-4 w-4 text-zinc-500" />
)}
{isReact ? <HeartFilledIcon className="h-4 w-4 group-hover:text-red-400" /> : <HeartIcon className="h-4 w-4 text-zinc-500" />}
</div>
<span>{reaction}</span>
</button>

View File

@ -2,12 +2,10 @@
import AccountBar from '@components/accountBar';
import ActiveLink from '@components/activeLink';
import { currentUser } from '@stores/currentUser';
import { useStore } from '@nanostores/react';
import { useLocalStorage } from '@rehooks/local-storage';
export default function UserLayout({ children }: { children: React.ReactNode }) {
const $currentUser: any = useStore(currentUser);
const [currentUser]: any = useLocalStorage('current-user');
return (
<div className="flex h-full w-full flex-row">
@ -27,13 +25,13 @@ export default function UserLayout({ children }: { children: React.ReactNode })
</div>
<div className="flex flex-col gap-1 text-zinc-500">
<ActiveLink
href={`/profile/${$currentUser.pubkey}`}
href={`/profile/${currentUser.pubkey}`}
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900">
<span>Personal Page</span>
</ActiveLink>
<ActiveLink
href={`/profile/update?pubkey=${$currentUser.pubkey}`}
href={`/profile/update?pubkey=${currentUser.pubkey}`}
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900">
<span>Update Profile</span>

View File

@ -2,9 +2,7 @@
import DatabaseProvider from '@components/contexts/database';
import RelayProvider from '@components/contexts/relay';
import { relays } from '@stores/relays';
import { useStore } from '@nanostores/react';
import { useLocalStorage } from '@rehooks/local-storage';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import { ReactElement, ReactNode } from 'react';
@ -23,12 +21,12 @@ type AppPropsWithLayout = AppProps & {
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
// Get all relays
const $relays = useStore(relays);
// Get relays from localstorage
const [relays] = useLocalStorage('relays');
return (
<DatabaseProvider>
<RelayProvider relays={$relays}>{getLayout(<Component {...pageProps} />)}</RelayProvider>
<RelayProvider relays={relays}>{getLayout(<Component {...pageProps} />)}</RelayProvider>
</DatabaseProvider>
);
}

View File

@ -2,90 +2,32 @@
import BaseLayout from '@layouts/baseLayout';
import FullLayout from '@layouts/fullLayout';
import { DatabaseContext } from '@components/contexts/database';
import { currentUser } from '@stores/currentUser';
import { follows } from '@stores/follows';
import LumeSymbol from '@assets/icons/Lume';
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification';
import { useLocalStorage } from '@rehooks/local-storage';
import { motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useContext, useEffect, useState } from 'react';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useEffect, useState } from 'react';
export default function Page() {
const db: any = useContext(DatabaseContext);
const router = useRouter();
const [currentUser]: any = useLocalStorage('current-user');
const [loading, setLoading] = useState(true);
const requestNotification = useCallback(async () => {
// NOTE: notification don't work in dev mode (only affect MacOS)
// ref: https://github.com/tauri-apps/tauri/issues/4965
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
const permission = await requestPermission();
permissionGranted = permission === 'granted';
}
if (permissionGranted) {
sendNotification({ title: 'Lume', body: 'Nostr is awesome' });
}
return permissionGranted;
}, []);
const getAccount = useCallback(async () => {
const result = await db.select(`SELECT * FROM accounts ASC LIMIT 1`);
return result;
}, [db]);
const getFollows = useCallback(
async (account: { id: string }) => {
const arr = [];
const result: any = await db.select(`SELECT pubkey FROM follows WHERE account = "${account.id}"`);
result.forEach((item: { pubkey: string }) => {
arr.push(item.pubkey);
});
return arr;
},
[db]
);
// Explain:
// Step 1: request allow notification from system
// Step 2: get first account. #TODO: get last used account instead (part of multi account feature)
// Step 3: get follows by account
useEffect(() => {
requestNotification().then(() => {
getAccount()
.then((res: any) => {
if (res.length === 0) {
setTimeout(() => {
setLoading(false);
router.push('/onboarding');
}, 1500);
} else {
// store current user in localstorage
currentUser.set(res[0]);
getFollows(res[0])
.then(async (res) => {
// store follows in localstorage
follows.set(res);
// redirect to newsfeed
setTimeout(() => {
setLoading(false);
router.push('/feed/following');
}, 1500);
})
.catch(console.error);
}
})
.catch(console.error);
});
}, [requestNotification, getAccount, getFollows, router]);
console.log(currentUser);
if (!currentUser) {
setTimeout(() => {
setLoading(false);
router.push('/onboarding');
}, 1500);
} else {
setTimeout(() => {
setLoading(false);
router.push('/feed/following');
}, 1500);
}
}, [currentUser, router]);
return (
<div className="relative flex h-full flex-col items-center justify-between">

View File

@ -5,16 +5,13 @@ import OnboardingLayout from '@layouts/onboardingLayout';
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
import { currentUser } from '@stores/currentUser';
import { relays } from '@stores/relays';
import { useStore } from '@nanostores/react';
import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
import { useLocalStorage, writeStorage } from '@rehooks/local-storage';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useState } from 'react';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useContext, useMemo, useState } from 'react';
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
const config: Config = {
@ -22,11 +19,12 @@ const config: Config = {
};
export default function Page() {
const db: any = useContext(DatabaseContext);
const router = useRouter();
const { db }: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const $relays = useStore(relays);
const [relays] = useLocalStorage('relays');
const [type, setType] = useState('password');
const [loading, setLoading] = useState(false);
@ -47,13 +45,22 @@ export default function Page() {
};
// auto-generated profile
const data = {
display_name: name,
name: name,
username: name.toLowerCase(),
picture: 'https://bafybeidfsbrzqbvontmucteomoz2rkrxugu462l5hyhh6uioslkfzzs4oq.ipfs.w3s.link/avatar-11.png',
banner: 'https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg',
};
const data = useMemo(
() => ({
display_name: name,
name: name,
username: name.toLowerCase(),
picture: 'https://bafybeidfsbrzqbvontmucteomoz2rkrxugu462l5hyhh6uioslkfzzs4oq.ipfs.w3s.link/avatar-11.png',
banner: 'https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg',
}),
[name]
);
const insertDB = useCallback(async () => {
await db.execute(
`INSERT INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubKey}", "${privKey}", "${npub}", "${nsec}", '${JSON.stringify(data)}')`
);
}, [data, db, npub, nsec, privKey, pubKey]);
const createAccount = async () => {
setLoading(true);
@ -68,27 +75,25 @@ export default function Page() {
};
event.id = getEventHash(event);
event.sig = signEvent(event, privKey);
// publish to relays
relayPool.publish(event, $relays);
// save account to database
await db.execute(
`INSERT INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubKey}", "${privKey}", "${npub}", "${nsec}", '${JSON.stringify(data)}')`
);
// set currentUser in global state
currentUser.set({
metadata: JSON.stringify(data),
npub: npub,
privkey: privKey,
pubkey: pubKey,
});
// redirect to pre-follow
setTimeout(() => {
setLoading(false);
router.push('/onboarding/following');
}, 1500);
insertDB()
.then(() => {
// publish to relays
relayPool.publish(event, relays);
// set currentUser in global state
writeStorage('current-user', {
metadata: JSON.stringify(data),
npub: npub,
privkey: privKey,
pubkey: pubKey,
});
// redirect to pre-follow
setTimeout(() => {
setLoading(false);
router.push('/onboarding/create/pre-follows');
}, 1500);
})
.catch(console.error);
};
return (

View File

@ -6,28 +6,25 @@ import { DatabaseContext } from '@components/contexts/database';
import { truncate } from '@utils/truncate';
import { currentUser } from '@stores/currentUser';
import data from '@assets/directory.json';
import { useStore } from '@nanostores/react';
import { CheckCircledIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { nip19 } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useState } from 'react';
const shuffle = (arr: { name: string; avatar: string; npub: string }[]) => [...arr].sort(() => Math.random() - 0.5);
export default function Page() {
const db: any = useContext(DatabaseContext);
const router = useRouter();
const shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5);
const [follow, setFollow] = useState([]);
const [loading, setLoading] = useState(false);
const [list] = useState(shuffle(data));
const $currentUser: any = useStore(currentUser);
const [currentUser]: any = useLocalStorage('current-user');
const followUser = (e) => {
const npub = e.currentTarget.getAttribute('data-npub');
@ -36,11 +33,11 @@ export default function Page() {
const insertDB = async () => {
// self follow
await db.execute(`INSERT INTO follows (pubkey, account, kind) VALUES ("${$currentUser.pubkey}", "${$currentUser.pubkey}", "0")`);
await db.execute(`INSERT INTO follows (pubkey, account, kind) VALUES ("${currentUser.pubkey}", "${currentUser.pubkey}", "0")`);
// follow selected
follow.forEach(async (npub) => {
const { data } = nip19.decode(npub);
await db.execute(`INSERT INTO follows (pubkey, account, kind) VALUES ("${data}", "${$currentUser.pubkey}", "0")`);
await db.execute(`INSERT INTO follows (pubkey, account, kind) VALUES ("${data}", "${currentUser.pubkey}", "0")`);
});
};

View File

@ -1,116 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
import { relays } from '@stores/relays';
import { useStore } from '@nanostores/react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { getPublicKey, nip19 } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useContext, useEffect, useState } from 'react';
export default function Page() {
const db: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const $relays = useStore(relays);
const router = useRouter();
const { privkey }: any = router.query;
const [account, setAccount] = useState(null);
const [loading, setLoading] = useState(false);
const pubkey = privkey ? getPublicKey(privkey) : null;
const npub = privkey ? nip19.npubEncode(pubkey) : null;
const nsec = privkey ? nip19.nsecEncode(privkey) : null;
relayPool.subscribe(
[
{
authors: [pubkey],
kinds: [0],
},
],
$relays,
(event: any) => {
const metadata = JSON.parse(event.content);
setAccount(metadata);
},
undefined,
(events: any, relayURL: any) => {
console.log(events, relayURL);
}
);
const insertDB = useCallback(async () => {
// save account to database
const metadata = JSON.stringify(account);
await db.execute(
`INSERT INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubkey}", "${privkey}", "${npub}", "${nsec}", '${metadata}')`
);
await db.close();
}, [account, db, npub, nsec, privkey, pubkey]);
useEffect(() => {
setLoading(true);
if (account !== null) {
insertDB()
.then(() => {
setTimeout(() => {
setLoading(false);
router.push({
pathname: '/onboarding/fetch-follows',
query: { pubkey: pubkey },
});
}, 1500);
})
.catch(console.error);
}
}, [account, insertDB, npub, nsec, privkey, pubkey, router]);
return (
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Fetching your profile...
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
As long as you have private key, you alway can sync your profile on every nostr client, so please keep your key safely
</motion.h2>
</div>
</motion.div>
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-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>
) : (
<></>
)}
</div>
</motion.div>
</div>
);
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>
<OnboardingLayout>{page}</OnboardingLayout>
</BaseLayout>
);
};

View File

@ -10,9 +10,7 @@ export default function Page() {
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<div className="flex flex-col gap-3">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Other social network require email/password
<br />
nostr use{' '}
@ -21,8 +19,8 @@ export default function Page() {
</span>
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
If you have used nostr before, you can import your own private key. Otherwise, you can
create a new key or use auto-generated account created by system.
If you have used nostr before, you can import your own private key. Otherwise, you can create a new key or use auto-generated account
created by system.
</motion.h2>
<motion.div layoutId="form"></motion.div>
<motion.div layoutId="action" className="mt-4 flex gap-2">
@ -32,7 +30,7 @@ export default function Page() {
Create new key
</Link>
<Link
href="/onboarding/import"
href="/onboarding/login"
className="hover:bg-zinc-900/2.5 transform rounded-lg border border-black/5 bg-zinc-800 px-3.5 py-2 font-medium ring-1 ring-inset ring-zinc-900/10 hover:text-zinc-900 active:translate-y-1 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-zinc-700 dark:hover:text-white">
Login with private key
</Link>
@ -44,13 +42,7 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -5,35 +5,69 @@ import OnboardingLayout from '@layouts/onboardingLayout';
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
import { relays } from '@stores/relays';
import { useStore } from '@nanostores/react';
import { useLocalStorage } from '@rehooks/local-storage';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useEffect, useState } from 'react';
import { getPublicKey, nip19 } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useContext, useMemo, useState } from 'react';
export default function Page() {
const db: any = useContext(DatabaseContext);
const { db }: any = useContext(DatabaseContext);
const relayPool: any = useContext(RelayContext);
const $relays = useStore(relays);
const [loading, setLoading] = useState(false);
const [relays] = useLocalStorage('relays');
const router = useRouter();
const { pubkey }: any = router.query;
const { privkey }: any = router.query;
const [follows, setFollows] = useState([null]);
const [loading, setLoading] = useState(false);
const pubkey = useMemo(() => (privkey ? getPublicKey(privkey) : null), [privkey]);
// save account to database
const insertAccount = useCallback(
async (metadata) => {
if (loading === false) {
const npub = privkey ? nip19.npubEncode(pubkey) : null;
const nsec = privkey ? nip19.nsecEncode(privkey) : null;
await db.execute(
`INSERT OR IGNORE INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubkey}", "${privkey}", "${npub}", "${nsec}", '${metadata}')`
);
setLoading(true);
}
},
[db, privkey, pubkey, loading]
);
// save follows to database
const insertFollows = useCallback(
async (follows) => {
follows.forEach(async (item) => {
if (item) {
await db.execute(`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${item[1]}", "${pubkey}", "0")`);
}
});
},
[db, pubkey]
);
relayPool.subscribe(
[
{
authors: [pubkey],
kinds: [0],
kinds: [0, 3],
since: 0,
},
],
$relays,
relays,
(event: any) => {
setFollows(event.tags);
if (event.kind === 0) {
insertAccount(event.content);
} else {
if (event.tags.length > 0) {
insertFollows(event.tags);
}
}
},
undefined,
(events: any, relayURL: any) => {
@ -41,39 +75,16 @@ export default function Page() {
}
);
useEffect(() => {
setLoading(true);
const insertDB = async () => {
follows.forEach(async (item) => {
if (item) {
await db.execute(`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${item[1]}", "${pubkey}", "0")`);
}
});
};
if (follows !== null && follows.length > 0) {
insertDB()
.then(() => {
setTimeout(() => {
setLoading(false);
router.push('/');
}, 1500);
})
.catch(console.error);
}
}, [db, follows, pubkey, router]);
return (
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Fetching your follows...
Fetching your profile...
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
Not only profile, every nostr client can sync your follows list when you move to a new client, so please keep your key safely (again)
As long as you have private key, you alway can sync your profile and follows list on every nostr client, so please keep your key safely
</motion.h2>
</div>
</motion.div>
@ -88,7 +99,11 @@ export default function Page() {
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
href="/"
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30">
<span className="drop-shadow-lg">Finish</span>
</Link>
)}
</div>
</motion.div>

View File

@ -44,7 +44,7 @@ export default function Page() {
try {
router.push({
pathname: '/onboarding/fetch-profile',
pathname: '/onboarding/login/fetch',
query: { privkey: privkey },
});
} catch (error) {

View File

@ -2,9 +2,7 @@
import BaseLayout from '@layouts/baseLayout';
import UserLayout from '@layouts/userLayout';
import { currentUser } from '@stores/currentUser';
import { useStore } from '@nanostores/react';
import { useLocalStorage } from '@rehooks/local-storage';
import { useRouter } from 'next/router';
import { dateToUnix, useNostr } from 'nostr-react';
import { getEventHash, signEvent } from 'nostr-tools';
@ -28,11 +26,8 @@ export default function Page() {
const { publish } = useNostr();
const [loading, setLoading] = useState(false);
const $currentUser: any = useStore(currentUser);
const profile =
$currentUser.metadata !== undefined
? JSON.parse($currentUser.metadata)
: { display_name: null, username: null };
const [currentUser]: any = useLocalStorage('current-user');
const profile = currentUser.metadata !== undefined ? JSON.parse(currentUser.metadata) : { display_name: null, username: null };
const {
register,
@ -48,28 +43,24 @@ export default function Page() {
content: JSON.stringify(data),
created_at: dateToUnix(),
kind: 0,
pubkey: $currentUser.pubkey,
pubkey: currentUser.pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, $currentUser.privkey);
event.sig = signEvent(event, currentUser.privkey);
publish(event);
// save account to database
const db = await Database.load('sqlite:lume.db');
await db.execute(
`UPDATE accounts SET metadata = '${JSON.stringify(data)}' WHERE pubkey = "${
$currentUser.pubkey
}"`
);
await db.execute(`UPDATE accounts SET metadata = '${JSON.stringify(data)}' WHERE pubkey = "${currentUser.pubkey}"`);
await db.close();
// set currentUser in global state
currentUser.set({
metadata: JSON.stringify(data),
npub: $currentUser.npub,
privkey: $currentUser.privkey,
pubkey: $currentUser.pubkey,
npub: currentUser.npub,
privkey: currentUser.privkey,
pubkey: currentUser.pubkey,
});
// redirect to newsfeed
@ -80,16 +71,11 @@ export default function Page() {
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col justify-between px-6">
<form onSubmit={handleSubmit(onSubmit)} className="flex h-full w-full flex-col justify-between px-6">
<div className="mb-8 flex flex-col gap-3 pt-8">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Update profile
</h1>
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">Update profile</h1>
<h2 className="w-3/4 text-zinc-400">
Your profile will be published to all relays, as long as you have the private key, you
always can recover your profile in any client
Your profile will be published to all relays, as long as you have the private key, you always can recover your profile in any client
</h2>
</div>
<fieldset className="flex flex-col gap-2">
@ -105,9 +91,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 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-sm text-red-400">
{errors.display_name && <p>{errors.display_name.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.display_name && <p>{errors.display_name.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -122,9 +106,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 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-sm text-red-400">
{errors.name && <p>{errors.name.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.name && <p>{errors.name.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -139,9 +121,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 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-sm text-red-400">
{errors.username && <p>{errors.username.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.username && <p>{errors.username.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -156,9 +136,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 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-sm text-red-400">
{errors.picture && <p>{errors.picture.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.picture && <p>{errors.picture.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -173,9 +151,7 @@ export default function Page() {
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 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-sm text-red-400">
{errors.banner && <p>{errors.banner.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.banner && <p>{errors.banner.message}</p>}</span>
</div>
</div>
<div className="grid grid-cols-4">
@ -190,27 +166,15 @@ export default function Page() {
className="relative h-24 w-full resize-none rounded-lg border border-black/5 px-3.5 py-2 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-sm text-red-400">
{errors.about && <p>{errors.about.message}</p>}
</span>
<span className="text-sm text-red-400">{errors.about && <p>{errors.about.message}</p>}</span>
</div>
</div>
</fieldset>
<div className="pb-5">
<div className="flex h-10 items-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>
<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"
@ -231,13 +195,7 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,10 +0,0 @@
import { persistentAtom } from '@nanostores/persistent';
export const currentUser = persistentAtom(
'currentUser',
{},
{
encode: JSON.stringify,
decode: JSON.parse,
}
);

View File

@ -1,10 +0,0 @@
import { persistentAtom } from '@nanostores/persistent';
export const follows = persistentAtom('follows', [], {
encode(value) {
return JSON.stringify(value);
},
decode(value) {
return JSON.parse(value);
},
});

View File

@ -1,25 +0,0 @@
import { persistentAtom } from '@nanostores/persistent';
export const relays = persistentAtom(
'relays',
[
'wss://relay.uselume.xyz',
'wss://nostr-pub.wellorder.net',
'wss://nostr.bongbong.com',
'wss://nostr.zebedee.cloud',
'wss://nostr.fmt.wiz.biz',
'wss://nostr.walletofsatoshi.com',
'wss://relay.snort.social',
'wss://offchain.pub',
'wss://nos.lol',
'wss://relay.damus.io',
],
{
encode(value) {
return JSON.stringify(value);
},
decode(value) {
return JSON.parse(value);
},
}
);

View File

@ -6,7 +6,6 @@
"@layouts/*": ["src/layouts/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@stores/*": ["src/stores/*"],
"@assets/*": ["src/assets/*"]
},
"target": "es2017",