diff --git a/src-tauri/migrations/20230418013219_initial_data.sql b/src-tauri/migrations/20230418013219_initial_data.sql index 1046ba20..6e71d3ea 100644 --- a/src-tauri/migrations/20230418013219_initial_data.sql +++ b/src-tauri/migrations/20230418013219_initial_data.sql @@ -6,13 +6,11 @@ CREATE TABLE accounts ( id INTEGER NOT NULL PRIMARY KEY, + npub TEXT NOT NULL UNIQUE, pubkey TEXT NOT NULL UNIQUE, privkey TEXT NOT NULL, + follows JSON, is_active INTEGER NOT NULL DEFAULT 0, - follows TEXT, - channels TEXT, - chats TEXT, - metadata TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -20,8 +18,18 @@ CREATE TABLE CREATE TABLE plebs ( id INTEGER NOT NULL PRIMARY KEY, - pubkey TEXT NOT NULL UNIQUE, - metadata TEXT, + npub TEXT NOT NULL UNIQUE, + display_name TEXT, + name TEXT, + username TEXT, + about TEXT, + bio TEXT, + website TEXT, + picture TEXT, + banner TEXT, + nip05 TEXT, + lud06 TEXT, + lud16 TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -33,10 +41,11 @@ CREATE TABLE account_id INTEGER NOT NULL, pubkey TEXT NOT NULL, kind INTEGER NOT NULL DEFAULT 1, - tags TEXT NOT NULL, + tags JSON, content TEXT NOT NULL, created_at INTEGER NOT NULL, parent_id TEXT, + parent_comment_id TEXT, FOREIGN KEY (account_id) REFERENCES accounts (id) ); @@ -45,7 +54,9 @@ CREATE TABLE channels ( id INTEGER NOT NULL PRIMARY KEY, event_id TEXT NOT NULL UNIQUE, - metadata TEXT NOT NULL, + name TEXT, + about TEXT, + picture TEXT, created_at INTEGER NOT NULL ); diff --git a/src-tauri/migrations/20230418080146_create_chats.sql b/src-tauri/migrations/20230418080146_create_chats.sql index 0e0ceb3b..551a1d4c 100644 --- a/src-tauri/migrations/20230418080146_create_chats.sql +++ b/src-tauri/migrations/20230418080146_create_chats.sql @@ -3,8 +3,9 @@ CREATE TABLE chats ( id INTEGER NOT NULL PRIMARY KEY, - account_id INTEGER NOT NULL, - pubkey TEXT NOT NULL UNIQUE, - created_at INTEGER NOT NULL, - FOREIGN KEY (account_id) REFERENCES accounts (id) + event_id TEXT NOT NULL UNIQUE, + receiver_pubkey INTEGER NOT NULL, + sender_pubkey TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL ); \ No newline at end of file diff --git a/src-tauri/migrations/20230425024708_add_default_channels.sql b/src-tauri/migrations/20230425024708_add_default_channels.sql index 04800643..eff03bf9 100644 --- a/src-tauri/migrations/20230425024708_add_default_channels.sql +++ b/src-tauri/migrations/20230425024708_add_default_channels.sql @@ -1,40 +1,57 @@ -- Add migration script here INSERT -OR IGNORE INTO channels (event_id, pubkey, metadata, created_at) +OR IGNORE INTO channels ( + event_id, + pubkey, + name, + about, + picture, + created_at +) VALUES ( "e3cadf5beca1b2af1cddaa41a633679bedf263e3de1eb229c6686c50d85df753", "126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f", - '{"name":"lume-general","picture":"https://void.cat/d/UNyxBmAh1MUx5gQTX95jyf.webp","about":"General channel for Lume"}', + "lume-general", + "General channel for Lume", + "https://void.cat/d/UNyxBmAh1MUx5gQTX95jyf.webp", 1681898574 ); INSERT -OR IGNORE INTO channels (event_id, pubkey, metadata, created_at) -VALUES - ( - "1abf8948d2fd05dd1836b33b324dca65138b2e80c77b27eeeed4323246efba4d", - "126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f", - '{"picture":"https://void.cat/d/MsqUKXXC4SxDfmT2KiHovJ.webp","name":"Arcade Open R&D","about":""}', - 1682252461 - ); - -INSERT -OR IGNORE INTO channels (event_id, pubkey, metadata, created_at) +OR IGNORE INTO channels ( + event_id, + pubkey, + name, + about, + picture, + created_at +) VALUES ( "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", - '{"about":"General discussion about the Amethyst Nostr client for Android","name":"Amethyst Users","picture":"https://nostr.build/i/5970.png"}', + "Amethyst Users", + "General discussion about the Amethyst Nostr client for Android", + "https://nostr.build/i/5970.png", 1674092111 ); INSERT -OR IGNORE INTO channels (event_id, pubkey, metadata, created_at) +OR IGNORE INTO channels ( + event_id, + pubkey, + name, + about, + picture, + created_at +) VALUES ( "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", "ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69", - '{"about":"","name":"Nostr","picture":"https://cloudflare-ipfs.com/ipfs/QmTN4Eas9atUULVbEAbUU8cowhtvK7g3t7jfKztY7wc8eP?.png"}', + "Nostr", + "", + "https://cloudflare-ipfs.com/ipfs/QmTN4Eas9atUULVbEAbUU8cowhtvK7g3t7jfKztY7wc8eP?.png", 1661333723 ); \ No newline at end of file diff --git a/src-tauri/migrations/20230427081017_clean_up_account.sql b/src-tauri/migrations/20230427081017_clean_up_account.sql deleted file mode 100644 index 5c5d8925..00000000 --- a/src-tauri/migrations/20230427081017_clean_up_account.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Add migration script here -ALTER TABLE accounts -DROP COLUMN channels; - -ALTER TABLE accounts -DROP COLUMN chats; \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 374dd170..d1fbfa89 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -87,12 +87,6 @@ fn main() { sql: include_str!("../migrations/20230425050745_add_blacklist_model.sql"), kind: MigrationKind::Up, }, - Migration { - version: 20230427081017, - description: "clean up account", - sql: include_str!("../migrations/20230427081017_clean_up_account.sql"), - kind: MigrationKind::Up, - }, Migration { version: 20230521092300, description: "create block", diff --git a/src/app/auth/components/user.tsx b/src/app/auth/components/user.tsx index 82584d56..67e44d27 100644 --- a/src/app/auth/components/user.tsx +++ b/src/app/auth/components/user.tsx @@ -1,11 +1,9 @@ import { Image } from "@shared/image"; - import { DEFAULT_AVATAR } from "@stores/constants"; - import { useProfile } from "@utils/hooks/useProfile"; import { shortenKey } from "@utils/shortenKey"; -export default function User({ pubkey }: { pubkey: string }) { +export function User({ pubkey }: { pubkey: string }) { const { user } = useProfile(pubkey); return ( @@ -15,7 +13,6 @@ export default function User({ pubkey }: { pubkey: string }) { src={user?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-md object-cover" - loading="lazy" decoding="async" /> diff --git a/src/app/auth/pages/create/index.page.tsx b/src/app/auth/pages/create/index.page.tsx index 085f0811..49198744 100644 --- a/src/app/auth/pages/create/index.page.tsx +++ b/src/app/auth/pages/create/index.page.tsx @@ -1,16 +1,12 @@ import EyeOffIcon from "@icons/eyeOff"; import EyeOnIcon from "@icons/eyeOn"; - -import { onboardingAtom } from "@stores/onboarding"; - -import { useSetAtom } from "jotai"; +import { createAccount } from "@utils/storage"; import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools"; import { useMemo, useState } from "react"; import { navigate } from "vite-plugin-ssr/client/router"; export function Page() { const [type, setType] = useState("password"); - const setOnboarding = useSetAtom(onboardingAtom); const privkey = useMemo(() => generatePrivateKey(), []); const pubkey = getPublicKey(privkey); @@ -26,9 +22,12 @@ export function Page() { } }; - const submit = () => { - setOnboarding((prev) => ({ ...prev, pubkey: pubkey, privkey: privkey })); - navigate("/app/auth/create/step-2"); + const submit = async () => { + const account = await createAccount(npub, pubkey, privkey, null, 1); + + if (account) { + navigate("/app/auth/create/step-2"); + } }; return ( diff --git a/src/app/auth/pages/create/step-2/index.page.tsx b/src/app/auth/pages/create/step-2/index.page.tsx index 3b8236dc..0747cf4d 100644 --- a/src/app/auth/pages/create/step-2/index.page.tsx +++ b/src/app/auth/pages/create/step-2/index.page.tsx @@ -1,38 +1,53 @@ import { AvatarUploader } from "@shared/avatarUploader"; import { Image } from "@shared/image"; - -import { DEFAULT_AVATAR } from "@stores/constants"; -import { onboardingAtom } from "@stores/onboarding"; - -import { useAtom } from "jotai"; -import { useEffect, useState } from "react"; +import { RelayContext } from "@shared/relayProvider"; +import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants"; +import { useActiveAccount } from "@utils/hooks/useActiveAccount"; +import { getEventHash, getSignature } from "nostr-tools"; +import { useContext, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { navigate } from "vite-plugin-ssr/client/router"; export function Page() { + const pool: any = useContext(RelayContext); + + const { account } = useActiveAccount(); + const [image, setImage] = useState(DEFAULT_AVATAR); const [loading, setLoading] = useState(false); - const [onboarding, setOnboarding] = useAtom(onboardingAtom); const { register, handleSubmit, setValue, formState: { isDirty, isValid }, - } = useForm({ - defaultValues: async () => { - if (onboarding.metadata) { - return onboarding.metadata; - } else { - return null; - } - }, - }); + } = useForm(); const onSubmit = (data: any) => { setLoading(true); - setOnboarding((prev) => ({ ...prev, metadata: data })); - navigate("/app/auth/create/step-3"); + + const event: any = { + content: JSON.stringify(data), + created_at: Math.floor(Date.now() / 1000), + kind: 0, + pubkey: account.pubkey, + tags: [], + }; + + event.id = getEventHash(event); + event.sig = getSignature(event, account.privkey); + + // publish + pool.publish(event, WRITEONLY_RELAYS); + + // redirect to step 3 + setTimeout( + () => + navigate("/app/auth/create/step-3", { + overwriteLastHistoryEntry: true, + }), + 2000, + ); }; useEffect(() => { diff --git a/src/app/auth/pages/create/step-3/index.page.tsx b/src/app/auth/pages/create/step-3/index.page.tsx index 9871ad55..dde35aaf 100644 --- a/src/app/auth/pages/create/step-3/index.page.tsx +++ b/src/app/auth/pages/create/step-3/index.page.tsx @@ -1,17 +1,11 @@ -import User from "@app/auth/components/user"; - -import { RelayContext } from "@shared/relayProvider"; - +import { User } from "@app/auth/components/user"; import CheckCircleIcon from "@icons/checkCircle"; - +import { RelayContext } from "@shared/relayProvider"; import { WRITEONLY_RELAYS } from "@stores/constants"; -import { onboardingAtom } from "@stores/onboarding"; - -import { createAccount, createPleb } from "@utils/storage"; +import { useActiveAccount } from "@utils/hooks/useActiveAccount"; +import { updateAccount } from "@utils/storage"; import { arrayToNIP02 } from "@utils/transform"; - -import { useAtom } from "jotai"; -import { getEventHash, signEvent } from "nostr-tools"; +import { getEventHash, getSignature } from "nostr-tools"; import { useContext, useState } from "react"; import { navigate } from "vite-plugin-ssr/client/router"; @@ -117,9 +111,10 @@ const initialList = [ export function Page() { const pool: any = useContext(RelayContext); + const { account } = useActiveAccount(); + const [loading, setLoading] = useState(false); const [follows, setFollows] = useState([]); - const [onboarding] = useAtom(onboardingAtom); // toggle follow state const toggleFollow = (pubkey: string) => { @@ -129,68 +124,37 @@ export function Page() { setFollows(arr); }; - const broadcastAccount = () => { - // build event - const event: any = { - content: JSON.stringify(onboarding.metadata), - created_at: Math.floor(Date.now() / 1000), - kind: 0, - pubkey: onboarding.pubkey, - tags: [], - }; - event.id = getEventHash(event); - event.sig = signEvent(event, onboarding.privkey); - // broadcast - pool.publish(event, WRITEONLY_RELAYS); - }; - - const broadcastContacts = () => { - const nip02 = arrayToNIP02(follows); - // build event - const event: any = { - content: "", - created_at: Math.floor(Date.now() / 1000), - kind: 3, - pubkey: onboarding.pubkey, - tags: nip02, - }; - event.id = getEventHash(event); - event.sig = signEvent(event, onboarding.privkey); - // broadcast - pool.publish(event, WRITEONLY_RELAYS); - }; - // save follows to database then broadcast const submit = async () => { setLoading(true); - const followsIncludeSelf = follows.concat([onboarding.pubkey]); - // insert to database - createAccount( - onboarding.pubkey, - onboarding.privkey, - onboarding.metadata, - arrayToNIP02(followsIncludeSelf), - 1, - ) - .then((res) => { - if (res) { - for (const tag of follows) { - fetch(`https://us.rbr.bio/${tag}/metadata.json`) - .then((data) => data.json()) - .then((data) => createPleb(tag, data ?? "")); - } - broadcastAccount(); - broadcastContacts(); - setTimeout( - () => navigate("/", { overwriteLastHistoryEntry: true }), - 2000, - ); - } else { - console.error(); - } - }) - .catch(console.error); + // update account follows + updateAccount("follows", follows, account.pubkey); + + const tags = arrayToNIP02(follows); + + const event: any = { + content: "", + created_at: Math.floor(Date.now() / 1000), + kind: 3, + pubkey: account.pubkey, + tags: tags, + }; + + event.id = getEventHash(event); + event.sig = getSignature(event, account.privkey); + + // publish + pool.publish(event, WRITEONLY_RELAYS); + + // redirect to step 3 + setTimeout( + () => + navigate("/app/prefetch", { + overwriteLastHistoryEntry: true, + }), + 2000, + ); }; return ( diff --git a/src/app/auth/pages/import/index.page.tsx b/src/app/auth/pages/import/index.page.tsx index f26714fc..3b5a703a 100644 --- a/src/app/auth/pages/import/index.page.tsx +++ b/src/app/auth/pages/import/index.page.tsx @@ -1,6 +1,4 @@ -import { onboardingAtom } from "@stores/onboarding"; - -import { useSetAtom } from "jotai"; +import { createAccount } from "@utils/storage"; import { getPublicKey, nip19 } from "nostr-tools"; import { Resolver, useForm } from "react-hook-form"; import { navigate } from "vite-plugin-ssr/client/router"; @@ -24,8 +22,6 @@ const resolver: Resolver = async (values) => { }; export function Page() { - const setOnboardingPrivkey = useSetAtom(onboardingAtom); - const { register, setError, @@ -42,8 +38,14 @@ export function Page() { } if (typeof getPublicKey(privkey) === "string") { - setOnboardingPrivkey((prev) => ({ ...prev, privkey: privkey })); - navigate("/app/auth/import/step-2"); + const pubkey = getPublicKey(privkey); + const npub = nip19.npubEncode(pubkey); + + const account = await createAccount(npub, pubkey, privkey, null, 1); + + if (account) { + navigate("/app/auth/import/step-2"); + } } } catch (error) { setError("key", { diff --git a/src/app/auth/pages/import/step-2/index.page.tsx b/src/app/auth/pages/import/step-2/index.page.tsx index d1a583c7..28a208ad 100644 --- a/src/app/auth/pages/import/step-2/index.page.tsx +++ b/src/app/auth/pages/import/step-2/index.page.tsx @@ -1,85 +1,55 @@ -import { Image } from "@shared/image"; +import { User } from "@app/auth/components/user"; import { RelayContext } from "@shared/relayProvider"; - -import { DEFAULT_AVATAR, READONLY_RELAYS } from "@stores/constants"; -import { onboardingAtom } from "@stores/onboarding"; - -import { shortenKey } from "@utils/shortenKey"; -import { createAccount, createPleb } from "@utils/storage"; - -import { useAtom } from "jotai"; -import { getPublicKey } from "nostr-tools"; -import { useContext, useMemo, useState } from "react"; +import { READONLY_RELAYS } from "@stores/constants"; +import { useActiveAccount } from "@utils/hooks/useActiveAccount"; +import { updateAccount } from "@utils/storage"; +import { nip02ToArray } from "@utils/transform"; +import { useContext, useState } from "react"; import useSWRSubscription from "swr/subscription"; import { navigate } from "vite-plugin-ssr/client/router"; export function Page() { const pool: any = useContext(RelayContext); + const { account } = useActiveAccount(); + const [loading, setLoading] = useState(false); - const [onboarding, setOnboarding] = useAtom(onboardingAtom); - const pubkey = useMemo( - () => (onboarding.privkey ? getPublicKey(onboarding.privkey) : ""), - [onboarding.privkey], - ); + const [follows, setFollows] = useState([]); - const { data, error } = useSWRSubscription( - pubkey ? pubkey : null, - (key, { next }) => { - const unsubscribe = pool.subscribe( - [ - { - kinds: [0, 3], - authors: [key], - }, - ], - READONLY_RELAYS, - (event: any) => { - switch (event.kind) { - case 0: - // update state - next(null, JSON.parse(event.content)); - // create account - setOnboarding((prev) => ({ ...prev, metadata: event.content })); - break; - case 3: - setOnboarding((prev) => ({ ...prev, follows: event.tags })); - break; - default: - break; - } + useSWRSubscription(account ? account.pubkey : null, (key: string) => { + const unsubscribe = pool.subscribe( + [ + { + kinds: [3], + authors: [key], }, - ); + ], + READONLY_RELAYS, + (event: any) => { + setFollows(event.tags); + }, + ); - return () => { - unsubscribe(); - }; - }, - ); + return () => { + unsubscribe(); + }; + }); const submit = () => { // show loading indicator setLoading(true); - const follows = onboarding.follows.concat([["p", pubkey]]); - // insert to database - createAccount(pubkey, onboarding.privkey, onboarding.metadata, follows, 1) - .then((res) => { - if (res) { - for (const tag of onboarding.follows) { - fetch(`https://rbr.bio/${tag[1]}/metadata.json`) - .then((data) => data.json()) - .then((data) => createPleb(tag[1], data ?? "")); - } - setTimeout( - () => navigate("/", { overwriteLastHistoryEntry: true }), - 2000, - ); - } else { - console.error(); - } - }) - .catch(console.error); + // follows as list + const followsList = nip02ToArray(follows); + + // update account follows + updateAccount("follows", followsList, account.pubkey); + + // redirect to home + setTimeout( + () => navigate("/app/prefetch", { overwriteLastHistoryEntry: true }), + 2000, + ); }; return ( @@ -91,8 +61,7 @@ export function Page() {
- {error &&
Failed to load profile
} - {!data ? ( + {!account ? (
@@ -104,21 +73,7 @@ export function Page() {
) : (
-
- {pubkey} -
-

- {data.display_name || data.name} -

-

- {data.nip05 || shortenKey(pubkey)} -

-
-
+
) : ( - chats.map((item: { pubkey: string }) => ( - + chats.map((item) => ( + )) )}
diff --git a/src/app/chat/components/messages/form.tsx b/src/app/chat/components/messages/form.tsx index 0052b5b8..49816c26 100644 --- a/src/app/chat/components/messages/form.tsx +++ b/src/app/chat/components/messages/form.tsx @@ -9,7 +9,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { useAtom } from "jotai"; import { useResetAtom } from "jotai/utils"; -import { getEventHash, nip04, signEvent } from "nostr-tools"; +import { getEventHash, getSignature, nip04 } from "nostr-tools"; import { useCallback, useContext } from "react"; export default function ChatMessageForm({ @@ -40,7 +40,7 @@ export default function ChatMessageForm({ tags: [["p", receiverPubkey]], }; event.id = getEventHash(event); - event.sig = signEvent(event, account.privkey); + event.sig = getSignature(event, account.privkey); // publish note pool.publish(event, WRITEONLY_RELAYS); // reset state diff --git a/src/app/chat/components/self.tsx b/src/app/chat/components/self.tsx index 0a1dbed8..91379fc0 100644 --- a/src/app/chat/components/self.tsx +++ b/src/app/chat/components/self.tsx @@ -15,7 +15,6 @@ export default function ChatsListSelfItem() { const pagePubkey = searchParams.pubkey; const { account, isLoading, isError } = useActiveAccount(); - const profile = account ? JSON.parse(account.metadata) : null; return ( <> @@ -39,14 +38,14 @@ export default function ChatsListSelfItem() { >
{account.pubkey}
- {profile?.nip05 || profile?.name || shortenKey(account.pubkey)} + {account?.nip05 || account?.name || shortenKey(account.pubkey)}
(you)
diff --git a/src/app/index/pages/index.page.tsx b/src/app/index/pages/index.page.tsx index 0c2f6594..31c349fa 100644 --- a/src/app/index/pages/index.page.tsx +++ b/src/app/index/pages/index.page.tsx @@ -1,23 +1,15 @@ -import { getActiveAccount } from "@utils/storage"; - -import useSWR from "swr"; +import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { navigate } from "vite-plugin-ssr/client/router"; -const fetcher = () => getActiveAccount(); - export function Page() { - const { data, isLoading } = useSWR("account", fetcher, { - revalidateIfStale: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - }); + const { account, isLoading } = useActiveAccount(); - if (!isLoading && !data) { + if (!isLoading && !account) { navigate("/app/auth", { overwriteLastHistoryEntry: true }); } - if (!isLoading && data) { - navigate("/app/inital-data", { overwriteLastHistoryEntry: true }); + if (!isLoading && account) { + navigate("/app/prefetch", { overwriteLastHistoryEntry: true }); } return ( diff --git a/src/app/note/components/metadata.tsx b/src/app/note/components/metadata.tsx index 1b7f2acf..c8a32ec2 100644 --- a/src/app/note/components/metadata.tsx +++ b/src/app/note/components/metadata.tsx @@ -1,7 +1,6 @@ import NoteReply from "@app/note/components/metadata/reply"; import NoteRepost from "@app/note/components/metadata/repost"; import NoteZap from "@app/note/components/metadata/zap"; -import ZapIcon from "@shared/icons/zap"; import { RelayContext } from "@shared/relayProvider"; import { READONLY_RELAYS } from "@stores/constants"; import { decode } from "light-bolt11-decoder"; diff --git a/src/app/note/components/metadata/like.tsx b/src/app/note/components/metadata/like.tsx index 2b073d97..3f074abb 100644 --- a/src/app/note/components/metadata/like.tsx +++ b/src/app/note/components/metadata/like.tsx @@ -7,7 +7,7 @@ import { WRITEONLY_RELAYS } from "@stores/constants"; import { dateToUnix } from "@utils/date"; import { useActiveAccount } from "@utils/hooks/useActiveAccount"; -import { getEventHash, signEvent } from "nostr-tools"; +import { getEventHash, getSignature } from "nostr-tools"; import { useContext, useEffect, useState } from "react"; export default function NoteLike({ @@ -35,7 +35,7 @@ export default function NoteLike({ pubkey: account.pubkey, }; event.id = getEventHash(event); - event.sig = signEvent(event, account.privkey); + event.sig = getSignature(event, account.privkey); // publish event to all relays pool.publish(event, WRITEONLY_RELAYS); // update state diff --git a/src/app/note/components/metadata/reply.tsx b/src/app/note/components/metadata/reply.tsx index 1d272ce9..30740a7c 100644 --- a/src/app/note/components/metadata/reply.tsx +++ b/src/app/note/components/metadata/reply.tsx @@ -10,7 +10,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { Dialog, Transition } from "@headlessui/react"; import { compactNumber } from "@utils/number"; -import { getEventHash, signEvent } from "nostr-tools"; +import { getEventHash, getSignature } from "nostr-tools"; import { Fragment, useContext, useEffect, useState } from "react"; export default function NoteReply({ @@ -24,7 +24,6 @@ export default function NoteReply({ const [value, setValue] = useState(""); const { account, isLoading, isError } = useActiveAccount(); - const profile = account ? JSON.parse(account.metadata) : null; const closeModal = () => { setIsOpen(false); @@ -44,7 +43,7 @@ export default function NoteReply({ tags: [["e", id]], }; event.id = getEventHash(event); - event.sig = signEvent(event, account.privkey); + event.sig = getSignature(event, account.privkey); // publish event pool.publish(event, WRITEONLY_RELAYS); @@ -106,7 +105,7 @@ export default function NoteReply({
user's avatar diff --git a/src/app/note/components/metadata/repost.tsx b/src/app/note/components/metadata/repost.tsx index 83ce75a1..f12dd7e9 100644 --- a/src/app/note/components/metadata/repost.tsx +++ b/src/app/note/components/metadata/repost.tsx @@ -8,7 +8,7 @@ import { dateToUnix } from "@utils/date"; import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { compactNumber } from "@utils/number"; -import { getEventHash, signEvent } from "nostr-tools"; +import { getEventHash, getSignature } from "nostr-tools"; import { useContext, useEffect, useState } from "react"; export default function NoteRepost({ @@ -36,7 +36,7 @@ export default function NoteRepost({ pubkey: account.pubkey, }; event.id = getEventHash(event); - event.sig = signEvent(event, account.privkey); + event.sig = getSignature(event, account.privkey); // publish event to all relays pool.publish(event, WRITEONLY_RELAYS); // update state diff --git a/src/app/note/components/replies/form.tsx b/src/app/note/components/replies/form.tsx index 8985cd4a..ec8ca266 100644 --- a/src/app/note/components/replies/form.tsx +++ b/src/app/note/components/replies/form.tsx @@ -6,15 +6,14 @@ import { WRITEONLY_RELAYS } from "@stores/constants"; import { dateToUnix } from "@utils/date"; import { useActiveAccount } from "@utils/hooks/useActiveAccount"; -import { getEventHash, signEvent } from "nostr-tools"; +import { getEventHash, getSignature } from "nostr-tools"; import { useContext, useState } from "react"; export default function NoteReplyForm({ id }: { id: string }) { const pool: any = useContext(RelayContext); - const { account, isLoading, isError } = useActiveAccount(); + const { account, isLoading, isError } = useActiveAccount(); const [value, setValue] = useState(""); - const profile = account ? JSON.parse(account.metadata) : null; const submitEvent = () => { if (!isLoading && !isError && account) { @@ -26,7 +25,7 @@ export default function NoteReplyForm({ id }: { id: string }) { tags: [["e", id]], }; event.id = getEventHash(event); - event.sig = signEvent(event, account.privkey); + event.sig = getSignature(event, account.privkey); // publish note pool.publish(event, WRITEONLY_RELAYS); @@ -42,7 +41,7 @@ export default function NoteReplyForm({ id }: { id: string }) {
{account?.pubkey} diff --git a/src/app/inital-data/pages/index.page.tsx b/src/app/prefetch/pages/index.page.tsx similarity index 60% rename from src/app/inital-data/pages/index.page.tsx rename to src/app/prefetch/pages/index.page.tsx index 8942758f..109d264a 100644 --- a/src/app/inital-data/pages/index.page.tsx +++ b/src/app/prefetch/pages/index.page.tsx @@ -1,23 +1,18 @@ -import { RelayContext } from "@shared/relayProvider"; - import LumeIcon from "@icons/lume"; - +import { RelayContext } from "@shared/relayProvider"; import { READONLY_RELAYS } from "@stores/constants"; - import { dateToUnix, getHourAgo } from "@utils/date"; +import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { addToBlacklist, - countTotalLongNotes, countTotalNotes, createChat, createNote, - getActiveAccount, getLastLogin, - updateLastLogin, } from "@utils/storage"; -import { getParentID, nip02ToArray } from "@utils/transform"; - -import { useContext, useEffect, useRef } from "react"; +import { getParentID } from "@utils/transform"; +import { useCallback, useContext, useRef } from "react"; +import useSWRSubscription from "swr/subscription"; import { navigate } from "vite-plugin-ssr/client/router"; function isJSON(str: string) { @@ -29,79 +24,68 @@ function isJSON(str: string) { return true; } +let lastLogin: string; +let totalNotes: number; + +if (typeof window !== "undefined") { + lastLogin = await getLastLogin(); + totalNotes = await countTotalNotes(); +} + export function Page() { const pool: any = useContext(RelayContext); + const now = useRef(new Date()); + const eose = useRef(0); - useEffect(() => { - let unsubscribe: () => void; - let timeout: any; + const { account, isLoading, isError } = useActiveAccount(); - const fetchInitalData = async () => { - const account = await getActiveAccount(); - const lastLogin = await getLastLogin(); - const notes = await countTotalNotes(); - const longNotes = await countTotalLongNotes(); + const getQuery = useCallback(() => { + const query = []; + const follows = JSON.parse(account.follows); - const follows = nip02ToArray(JSON.parse(account.follows)); - const query = []; + let queryNoteSince: number; + let querySince: number; - let sinceNotes: number; - let sinceLongNotes: number; - - if (notes === 0) { - sinceNotes = dateToUnix(getHourAgo(48, now.current)); + if (totalNotes === 0) { + queryNoteSince = dateToUnix(getHourAgo(48, now.current)); + } else { + if (parseInt(lastLogin) > 0) { + queryNoteSince = parseInt(lastLogin); } else { - if (parseInt(lastLogin) > 0) { - sinceNotes = parseInt(lastLogin); - } else { - sinceNotes = dateToUnix(getHourAgo(48, now.current)); - } + queryNoteSince = dateToUnix(getHourAgo(48, now.current)); } + } - if (longNotes === 0) { - sinceLongNotes = 0; - } else { - if (parseInt(lastLogin) > 0) { - sinceLongNotes = parseInt(lastLogin); - } else { - sinceLongNotes = 0; - } - } + // kind 1 (notes) query + query.push({ + kinds: [1, 6, 1063], + authors: follows, + since: queryNoteSince, + }); - // kind 1 (notes) query - query.push({ - kinds: [1, 6, 1063], - authors: follows, - since: sinceNotes, - until: dateToUnix(now.current), - }); + // kind 4 (chats) query + query.push({ + kinds: [4], + "#p": [account.pubkey], + since: querySince, + }); - // kind 4 (chats) query - query.push({ - kinds: [4], - "#p": [account.pubkey], - since: 0, - until: dateToUnix(now.current), - }); + // kind 43, 43 (mute user, hide message) query + query.push({ + authors: [account.pubkey], + kinds: [43, 44], + since: querySince, + }); - // kind 43, 43 (mute user, hide message) query - query.push({ - authors: [account.pubkey], - kinds: [43, 44], - since: 0, - until: dateToUnix(now.current), - }); + return query; + }, [account.follows]); - // kind 30023 (long post) query - query.push({ - kinds: [30023], - since: sinceLongNotes, - until: dateToUnix(now.current), - }); - - // subscribe relays - unsubscribe = pool.subscribe( + useSWRSubscription( + !isLoading && !isError && account ? "prefetch" : null, + () => { + const query = getQuery(); + const unsubscribe = pool.subscribe( query, READONLY_RELAYS, (event: any) => { @@ -124,9 +108,13 @@ export function Page() { } // chat case 4: - if (event.pubkey !== account.pubkey) { - createChat(account.id, event.pubkey, event.created_at); - } + createChat( + event.id, + account.pubkey, + event.pubkey, + event.content, + event.created_at, + ); break; // repost case 6: @@ -165,47 +153,24 @@ export function Page() { "", ); break; - // long post - case 30023: { - // insert event to local database - const verifyMetadata = isJSON(event.tags); - if (verifyMetadata) { - createNote( - event.id, - account.id, - event.pubkey, - event.kind, - event.tags, - event.content, - event.created_at, - "", - ); - } - break; - } default: break; } }, undefined, () => { - updateLastLogin(dateToUnix(now.current)); - timeout = setTimeout(() => { + eose.current += 1; + if (eose.current === READONLY_RELAYS.length) { navigate("/app/space", { overwriteLastHistoryEntry: true }); - }, 5000); + } }, ); - }; - fetchInitalData().catch(console.error); - - return () => { - if (unsubscribe) { + return () => { unsubscribe(); - } - clearTimeout(timeout); - }; - }, [pool]); + }; + }, + ); return (
diff --git a/src/renderer/_default.page.client.tsx b/src/renderer/_default.page.client.tsx index eafc578d..346dfd84 100644 --- a/src/renderer/_default.page.client.tsx +++ b/src/renderer/_default.page.client.tsx @@ -1,10 +1,9 @@ -import { StrictMode } from "react"; -import { Root, createRoot, hydrateRoot } from "react-dom/client"; -import "vidstack/styles/defaults.css"; - import "./index.css"; import { Shell } from "./shell"; import { PageContextClient } from "./types"; +import { StrictMode } from "react"; +import { Root, createRoot, hydrateRoot } from "react-dom/client"; +import "vidstack/styles/defaults.css"; export const clientRouting = true; export const hydrationCanBeAborted = true; diff --git a/src/renderer/_default.page.server.tsx b/src/renderer/_default.page.server.tsx index ce6090fa..52a010e3 100644 --- a/src/renderer/_default.page.server.tsx +++ b/src/renderer/_default.page.server.tsx @@ -1,10 +1,9 @@ +import { Shell } from "./shell"; +import { PageContextServer } from "./types"; import { StrictMode } from "react"; import ReactDOMServer from "react-dom/server"; import { dangerouslySkipEscape, escapeInject } from "vite-plugin-ssr/server"; -import { Shell } from "./shell"; -import { PageContextServer } from "./types"; - export const passToClient = ["pageProps"]; export function render(pageContext: PageContextServer) { diff --git a/src/renderer/shell.tsx b/src/renderer/shell.tsx index 00163ec6..853b7583 100644 --- a/src/renderer/shell.tsx +++ b/src/renderer/shell.tsx @@ -1,11 +1,8 @@ -import { RelayProvider } from "@shared/relayProvider"; - -import { PageContextProvider } from "@utils/hooks/usePageContext"; - -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; - import { LayoutDefault } from "./layoutDefault"; import { PageContext } from "./types"; +import { RelayProvider } from "@shared/relayProvider"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { PageContextProvider } from "@utils/hooks/usePageContext"; const queryClient = new QueryClient(); diff --git a/src/shared/accounts/active.tsx b/src/shared/accounts/active.tsx index e3774207..d89119ac 100644 --- a/src/shared/accounts/active.tsx +++ b/src/shared/accounts/active.tsx @@ -1,15 +1,16 @@ import { Image } from "@shared/image"; import { DEFAULT_AVATAR } from "@stores/constants"; +import { useProfile } from "@utils/hooks/useProfile"; -export default function ActiveAccount({ user }: { user: any }) { - const userData = JSON.parse(user.metadata); +export default function ActiveAccount({ data }: { data: any }) { + const { user } = useProfile(data.npub); return ( diff --git a/src/shared/accounts/inactive.tsx b/src/shared/accounts/inactive.tsx index f0bee53b..33ea14d0 100644 --- a/src/shared/accounts/inactive.tsx +++ b/src/shared/accounts/inactive.tsx @@ -1,15 +1,16 @@ import { Image } from "@shared/image"; import { DEFAULT_AVATAR } from "@stores/constants"; +import { useProfile } from "@utils/hooks/useProfile"; -export default function InactiveAccount({ user }: { user: any }) { - const userData = JSON.parse(user.metadata); +export default function InactiveAccount({ data }: { data: any }) { + const { user } = useProfile(data.npub); return (
user's avatar
diff --git a/src/shared/composer/types/post.tsx b/src/shared/composer/types/post.tsx index e962c865..ebfe90e9 100644 --- a/src/shared/composer/types/post.tsx +++ b/src/shared/composer/types/post.tsx @@ -6,7 +6,7 @@ import { WRITEONLY_RELAYS } from "@stores/constants"; import { dateToUnix } from "@utils/date"; -import { getEventHash, signEvent } from "nostr-tools"; +import { getEventHash, getSignature } from "nostr-tools"; import { useCallback, useContext, useMemo, useState } from "react"; import { Node, Transforms, createEditor } from "slate"; import { withHistory } from "slate-history"; @@ -91,7 +91,7 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) { tags: [], }; event.id = getEventHash(event); - event.sig = signEvent(event, privkey); + event.sig = getSignature(event, privkey); // publish note pool.publish(event, WRITEONLY_RELAYS); diff --git a/src/shared/eventCollector.tsx b/src/shared/eventCollector.tsx index 73d0e395..678b9747 100644 --- a/src/shared/eventCollector.tsx +++ b/src/shared/eventCollector.tsx @@ -44,7 +44,7 @@ export default function EventCollector() { since: dateToUnix(now.current), }, { - kinds: [0, 3], + kinds: [3], authors: [key.pubkey], }, { @@ -60,10 +60,6 @@ export default function EventCollector() { READONLY_RELAYS, (event: any) => { switch (event.kind) { - // metadata - case 0: - updateAccount("metadata", event.content, event.pubkey); - break; // short text note case 1: { const parentID = getParentID(event.tags, event.id); @@ -82,15 +78,21 @@ export default function EventCollector() { break; } // contacts - case 3: + case 3: { + const follows = nip02ToArray(event.tags); // update account's folllows with NIP-02 tag list - updateAccount("follows", event.tags, event.pubkey); + updateAccount("follows", follows, event.pubkey); break; + } // chat case 4: - if (event.pubkey !== key.pubkey) { - createChat(key.id, event.pubkey, event.created_at); - } + createChat( + event.id, + key.pubkey, + event.pubkey, + event.content, + event.created_at, + ); break; // repost case 6: diff --git a/src/shared/multiAccounts.tsx b/src/shared/multiAccounts.tsx index df0af826..5e21bd50 100644 --- a/src/shared/multiAccounts.tsx +++ b/src/shared/multiAccounts.tsx @@ -25,7 +25,7 @@ export default function MultiAccounts() { {!activeAccount ? (
) : ( - + )}
@@ -49,7 +49,7 @@ export default function MultiAccounts() { ) : ( accounts.map( (account: { is_active: number; pubkey: string }) => ( - + ), ) )} diff --git a/src/shared/relayProvider.tsx b/src/shared/relayProvider.tsx index b8e304c3..e9708061 100644 --- a/src/shared/relayProvider.tsx +++ b/src/shared/relayProvider.tsx @@ -1,5 +1,4 @@ import { FULL_RELAYS } from "@stores/constants"; - import { RelayPool } from "nostr-relaypool"; import { createContext } from "react"; diff --git a/src/stores/constants.tsx b/src/stores/constants.tsx index 06cb1f91..ac355758 100644 --- a/src/stores/constants.tsx +++ b/src/stores/constants.tsx @@ -6,12 +6,12 @@ export const DEFAULT_CHANNEL_BANNER = "https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg"; // metadata service -export const METADATA_SERVICE = "https://rbr.bio"; +export const METADATA_SERVICE = "https://us.rbr.bio"; // read-only relay list export const READONLY_RELAYS = [ "wss://welcome.nostr.wine", - "wss://relay.nostr.band/all", + "wss://relay.nostr.band", ]; // write-only relay list @@ -23,6 +23,6 @@ export const WRITEONLY_RELAYS = [ // full-relay list export const FULL_RELAYS = [ "wss://welcome.nostr.wine", - "wss://relay.nostr.band/all", + "wss://relay.nostr.band", "wss://nostr.mutinywallet.com", ]; diff --git a/src/utils/hooks/useActiveAccount.tsx b/src/utils/hooks/useActiveAccount.tsx index 9f3ce132..1db28ae5 100644 --- a/src/utils/hooks/useActiveAccount.tsx +++ b/src/utils/hooks/useActiveAccount.tsx @@ -1,5 +1,4 @@ import { getActiveAccount } from "@utils/storage"; - import useSWR from "swr"; const fetcher = () => getActiveAccount(); diff --git a/src/utils/hooks/useProfile.tsx b/src/utils/hooks/useProfile.tsx index 9a917649..d4c37dfb 100644 --- a/src/utils/hooks/useProfile.tsx +++ b/src/utils/hooks/useProfile.tsx @@ -1,33 +1,47 @@ import { METADATA_SERVICE } from "@stores/constants"; - import { createPleb, getPleb } from "@utils/storage"; - +import { nip19 } from "nostr-tools"; import useSWR from "swr"; -const fetcher = async (pubkey: string) => { - const result = await getPleb(pubkey); - if (result) { - const metadata = JSON.parse(result["metadata"]); - result["content"] = metadata.content; - result["metadata"] = undefined; +const fetcher = async (key: string) => { + let npub: string; + if (key.substring(0, 4) === "npub") { + npub = key; + } else { + npub = nip19.npubEncode(key); + } + + const current = Math.floor(Date.now() / 1000); + const result = await getPleb(npub); + + if (result && result.created_at + 86400 < current) { return result; } else { - const result = await fetch(`${METADATA_SERVICE}/${pubkey}/metadata.json`); - const resultJSON = await result.json(); - const cache = await createPleb(pubkey, resultJSON); + const res = await fetch(`${METADATA_SERVICE}/${key}/metadata.json`); - if (cache) { - return resultJSON; + if (!res.ok) { + return null; + } + + const json = await res.json(); + const saveToDB = await createPleb(key, json); + + if (saveToDB) { + return JSON.parse(json.content); } } }; -export function useProfile(pubkey: string) { - const { data, error, isLoading } = useSWR(pubkey, fetcher); +export function useProfile(key: string) { + const { data, error, isLoading } = useSWR(key, fetcher, { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: true, + }); return { - user: data ? JSON.parse(data.content ? data.content : null) : null, + user: data, isLoading, isError: error, }; diff --git a/src/utils/storage.tsx b/src/utils/storage.tsx index adbadb8a..3da2aa78 100644 --- a/src/utils/storage.tsx +++ b/src/utils/storage.tsx @@ -1,3 +1,4 @@ +import { nip19 } from "nostr-tools"; import Database from "tauri-plugin-sql-api"; let db: null | Database = null; @@ -15,10 +16,7 @@ export async function connect(): Promise { // get active account export async function getActiveAccount() { const db = await connect(); - // #TODO: check is_active == true - const result = await db.select( - "SELECT * FROM accounts WHERE is_active = 1 LIMIT 1;", - ); + const result = await db.select("SELECT * FROM accounts WHERE is_active = 1;"); return result[0]; } @@ -32,16 +30,16 @@ export async function getAccounts() { // create account export async function createAccount( + npub: string, pubkey: string, privkey: string, - metadata: string, follows?: string[][], is_active?: number, ) { const db = await connect(); return await db.execute( - "INSERT OR IGNORE INTO accounts (pubkey, privkey, metadata, follows, is_active) VALUES (?, ?, ?, ?, ?);", - [pubkey, privkey, metadata, follows || "", is_active || 0], + "INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);", + [npub, pubkey, privkey, follows || "", is_active || 0], ); } @@ -65,20 +63,47 @@ export async function getPlebs() { } // get pleb by pubkey -export async function getPleb(pubkey: string) { +export async function getPleb(npub: string) { const db = await connect(); - const result = await db.select( - `SELECT * FROM plebs WHERE pubkey = "${pubkey}"`, - ); - return result[0]; + const result = await db.select(`SELECT * FROM plebs WHERE npub = "${npub}";`); + + if (result) { + return result[0]; + } else { + return null; + } } // create pleb -export async function createPleb(pubkey: string, metadata: string) { +export async function createPleb(key: string, json: any) { const db = await connect(); + const data = JSON.parse(json.content); + + let npub: string; + + if (key.substring(0, 4) === "npub") { + npub = key; + } else { + npub = nip19.npubEncode(key); + } + return await db.execute( - "INSERT OR IGNORE INTO plebs (pubkey, metadata) VALUES (?, ?);", - [pubkey, metadata], + "INSERT OR REPLACE INTO plebs (npub, display_name, name, username, about, bio, website, picture, banner, nip05, lud06, lud16, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", + [ + npub, + data.display_name || data.displayName, + data.name, + data.username, + data.about, + data.bio, + data.website, + data.picture || data.image, + data.banner, + data.nip05, + data.lud06, + data.lud16, + data.created_at, + ], ); } @@ -239,30 +264,34 @@ export async function createChannel( // update channel metadata export async function updateChannelMetadata(event_id: string, value: string) { const db = await connect(); + const data = JSON.parse(value); + return await db.execute( - "UPDATE channels SET metadata = ? WHERE event_id = ?;", - [value, event_id], + "UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;", + [data.name, data.picture, data.about, event_id], ); } // get all chats -export async function getChats(account_id: number) { +export async function getChatsByPubkey(pubkey: string) { const db = await connect(); return await db.select( - `SELECT * FROM chats WHERE account_id <= "${account_id}" ORDER BY created_at DESC;`, + `SELECT DISTINCT sender_pubkey FROM chats WHERE receiver_pubkey = "${pubkey}" ORDER BY created_at DESC;`, ); } // create chat export async function createChat( - account_id: number, - pubkey: string, + event_id: string, + receiver_pubkey: string, + sender_pubkey: string, + content: string, created_at: number, ) { const db = await connect(); return await db.execute( - "INSERT OR IGNORE INTO chats (account_id, pubkey, created_at) VALUES (?, ?, ?);", - [account_id, pubkey, created_at], + "INSERT OR IGNORE INTO chats (event_id, receiver_pubkey, sender_pubkey, content, created_at) VALUES (?, ?, ?, ?, ?);", + [event_id, receiver_pubkey, sender_pubkey, content, created_at], ); }