This commit is contained in:
Ren Amamiya 2023-05-26 09:28:49 +07:00
parent 225179dd6d
commit 5c7b18bf29
41 changed files with 404 additions and 461 deletions

View File

@ -6,13 +6,11 @@
CREATE TABLE CREATE TABLE
accounts ( accounts (
id INTEGER NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
npub TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL UNIQUE, pubkey TEXT NOT NULL UNIQUE,
privkey TEXT NOT NULL, privkey TEXT NOT NULL,
follows JSON,
is_active INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 0,
follows TEXT,
channels TEXT,
chats TEXT,
metadata TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
@ -20,8 +18,18 @@ CREATE TABLE
CREATE TABLE CREATE TABLE
plebs ( plebs (
id INTEGER NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
pubkey TEXT NOT NULL UNIQUE, npub TEXT NOT NULL UNIQUE,
metadata TEXT, 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 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
@ -33,10 +41,11 @@ CREATE TABLE
account_id INTEGER NOT NULL, account_id INTEGER NOT NULL,
pubkey TEXT NOT NULL, pubkey TEXT NOT NULL,
kind INTEGER NOT NULL DEFAULT 1, kind INTEGER NOT NULL DEFAULT 1,
tags TEXT NOT NULL, tags JSON,
content TEXT NOT NULL, content TEXT NOT NULL,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
parent_id TEXT, parent_id TEXT,
parent_comment_id TEXT,
FOREIGN KEY (account_id) REFERENCES accounts (id) FOREIGN KEY (account_id) REFERENCES accounts (id)
); );
@ -45,7 +54,9 @@ CREATE TABLE
channels ( channels (
id INTEGER NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE, event_id TEXT NOT NULL UNIQUE,
metadata TEXT NOT NULL, name TEXT,
about TEXT,
picture TEXT,
created_at INTEGER NOT NULL created_at INTEGER NOT NULL
); );

View File

@ -3,8 +3,9 @@
CREATE TABLE CREATE TABLE
chats ( chats (
id INTEGER NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL, event_id TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL UNIQUE, receiver_pubkey INTEGER NOT NULL,
created_at INTEGER NOT NULL, sender_pubkey TEXT NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts (id) content TEXT NOT NULL,
created_at INTEGER NOT NULL
); );

View File

@ -1,40 +1,57 @@
-- Add migration script here -- Add migration script here
INSERT INSERT
OR IGNORE INTO channels (event_id, pubkey, metadata, created_at) OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES VALUES
( (
"e3cadf5beca1b2af1cddaa41a633679bedf263e3de1eb229c6686c50d85df753", "e3cadf5beca1b2af1cddaa41a633679bedf263e3de1eb229c6686c50d85df753",
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f", "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 1681898574
); );
INSERT INSERT
OR IGNORE INTO channels (event_id, pubkey, metadata, created_at) OR IGNORE INTO channels (
VALUES event_id,
( pubkey,
"1abf8948d2fd05dd1836b33b324dca65138b2e80c77b27eeeed4323246efba4d", name,
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f", about,
'{"picture":"https://void.cat/d/MsqUKXXC4SxDfmT2KiHovJ.webp","name":"Arcade Open R&D","about":""}', picture,
1682252461 created_at
); )
INSERT
OR IGNORE INTO channels (event_id, pubkey, metadata, created_at)
VALUES VALUES
( (
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "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 1674092111
); );
INSERT INSERT
OR IGNORE INTO channels (event_id, pubkey, metadata, created_at) OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES VALUES
( (
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
"ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69", "ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69",
'{"about":"","name":"Nostr","picture":"https://cloudflare-ipfs.com/ipfs/QmTN4Eas9atUULVbEAbUU8cowhtvK7g3t7jfKztY7wc8eP?.png"}', "Nostr",
"",
"https://cloudflare-ipfs.com/ipfs/QmTN4Eas9atUULVbEAbUU8cowhtvK7g3t7jfKztY7wc8eP?.png",
1661333723 1661333723
); );

View File

@ -1,6 +0,0 @@
-- Add migration script here
ALTER TABLE accounts
DROP COLUMN channels;
ALTER TABLE accounts
DROP COLUMN chats;

View File

@ -87,12 +87,6 @@ fn main() {
sql: include_str!("../migrations/20230425050745_add_blacklist_model.sql"), sql: include_str!("../migrations/20230425050745_add_blacklist_model.sql"),
kind: MigrationKind::Up, kind: MigrationKind::Up,
}, },
Migration {
version: 20230427081017,
description: "clean up account",
sql: include_str!("../migrations/20230427081017_clean_up_account.sql"),
kind: MigrationKind::Up,
},
Migration { Migration {
version: 20230521092300, version: 20230521092300,
description: "create block", description: "create block",

View File

@ -1,11 +1,9 @@
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile"; import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey"; import { shortenKey } from "@utils/shortenKey";
export default function User({ pubkey }: { pubkey: string }) { export function User({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
return ( return (
@ -15,7 +13,6 @@ export default function User({ pubkey }: { pubkey: string }) {
src={user?.picture || DEFAULT_AVATAR} src={user?.picture || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
loading="lazy"
decoding="async" decoding="async"
/> />
</div> </div>

View File

@ -1,16 +1,12 @@
import EyeOffIcon from "@icons/eyeOff"; import EyeOffIcon from "@icons/eyeOff";
import EyeOnIcon from "@icons/eyeOn"; import EyeOnIcon from "@icons/eyeOn";
import { createAccount } from "@utils/storage";
import { onboardingAtom } from "@stores/onboarding";
import { useSetAtom } from "jotai";
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools"; import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
export function Page() { export function Page() {
const [type, setType] = useState("password"); const [type, setType] = useState("password");
const setOnboarding = useSetAtom(onboardingAtom);
const privkey = useMemo(() => generatePrivateKey(), []); const privkey = useMemo(() => generatePrivateKey(), []);
const pubkey = getPublicKey(privkey); const pubkey = getPublicKey(privkey);
@ -26,9 +22,12 @@ export function Page() {
} }
}; };
const submit = () => { const submit = async () => {
setOnboarding((prev) => ({ ...prev, pubkey: pubkey, privkey: privkey })); const account = await createAccount(npub, pubkey, privkey, null, 1);
navigate("/app/auth/create/step-2");
if (account) {
navigate("/app/auth/create/step-2");
}
}; };
return ( return (

View File

@ -1,38 +1,53 @@
import { AvatarUploader } from "@shared/avatarUploader"; import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import { DEFAULT_AVATAR } from "@stores/constants"; import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants";
import { onboardingAtom } from "@stores/onboarding"; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, getSignature } from "nostr-tools";
import { useAtom } from "jotai"; import { useContext, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
export function Page() { export function Page() {
const pool: any = useContext(RelayContext);
const { account } = useActiveAccount();
const [image, setImage] = useState(DEFAULT_AVATAR); const [image, setImage] = useState(DEFAULT_AVATAR);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [onboarding, setOnboarding] = useAtom(onboardingAtom);
const { const {
register, register,
handleSubmit, handleSubmit,
setValue, setValue,
formState: { isDirty, isValid }, formState: { isDirty, isValid },
} = useForm({ } = useForm();
defaultValues: async () => {
if (onboarding.metadata) {
return onboarding.metadata;
} else {
return null;
}
},
});
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
setLoading(true); 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(() => { useEffect(() => {

View File

@ -1,17 +1,11 @@
import User from "@app/auth/components/user"; import { User } from "@app/auth/components/user";
import { RelayContext } from "@shared/relayProvider";
import CheckCircleIcon from "@icons/checkCircle"; import CheckCircleIcon from "@icons/checkCircle";
import { RelayContext } from "@shared/relayProvider";
import { WRITEONLY_RELAYS } from "@stores/constants"; import { WRITEONLY_RELAYS } from "@stores/constants";
import { onboardingAtom } from "@stores/onboarding"; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { updateAccount } from "@utils/storage";
import { createAccount, createPleb } from "@utils/storage";
import { arrayToNIP02 } from "@utils/transform"; import { arrayToNIP02 } from "@utils/transform";
import { getEventHash, getSignature } from "nostr-tools";
import { useAtom } from "jotai";
import { getEventHash, signEvent } from "nostr-tools";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
@ -117,9 +111,10 @@ const initialList = [
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { account } = useActiveAccount();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [follows, setFollows] = useState([]); const [follows, setFollows] = useState([]);
const [onboarding] = useAtom(onboardingAtom);
// toggle follow state // toggle follow state
const toggleFollow = (pubkey: string) => { const toggleFollow = (pubkey: string) => {
@ -129,68 +124,37 @@ export function Page() {
setFollows(arr); 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 // save follows to database then broadcast
const submit = async () => { const submit = async () => {
setLoading(true); setLoading(true);
const followsIncludeSelf = follows.concat([onboarding.pubkey]); // update account follows
// insert to database updateAccount("follows", follows, account.pubkey);
createAccount(
onboarding.pubkey, const tags = arrayToNIP02(follows);
onboarding.privkey,
onboarding.metadata, const event: any = {
arrayToNIP02(followsIncludeSelf), content: "",
1, created_at: Math.floor(Date.now() / 1000),
) kind: 3,
.then((res) => { pubkey: account.pubkey,
if (res) { tags: tags,
for (const tag of follows) { };
fetch(`https://us.rbr.bio/${tag}/metadata.json`)
.then((data) => data.json()) event.id = getEventHash(event);
.then((data) => createPleb(tag, data ?? "")); event.sig = getSignature(event, account.privkey);
}
broadcastAccount(); // publish
broadcastContacts(); pool.publish(event, WRITEONLY_RELAYS);
setTimeout(
() => navigate("/", { overwriteLastHistoryEntry: true }), // redirect to step 3
2000, setTimeout(
); () =>
} else { navigate("/app/prefetch", {
console.error(); overwriteLastHistoryEntry: true,
} }),
}) 2000,
.catch(console.error); );
}; };
return ( return (

View File

@ -1,6 +1,4 @@
import { onboardingAtom } from "@stores/onboarding"; import { createAccount } from "@utils/storage";
import { useSetAtom } from "jotai";
import { getPublicKey, nip19 } from "nostr-tools"; import { getPublicKey, nip19 } from "nostr-tools";
import { Resolver, useForm } from "react-hook-form"; import { Resolver, useForm } from "react-hook-form";
import { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
@ -24,8 +22,6 @@ const resolver: Resolver<FormValues> = async (values) => {
}; };
export function Page() { export function Page() {
const setOnboardingPrivkey = useSetAtom(onboardingAtom);
const { const {
register, register,
setError, setError,
@ -42,8 +38,14 @@ export function Page() {
} }
if (typeof getPublicKey(privkey) === "string") { if (typeof getPublicKey(privkey) === "string") {
setOnboardingPrivkey((prev) => ({ ...prev, privkey: privkey })); const pubkey = getPublicKey(privkey);
navigate("/app/auth/import/step-2"); const npub = nip19.npubEncode(pubkey);
const account = await createAccount(npub, pubkey, privkey, null, 1);
if (account) {
navigate("/app/auth/import/step-2");
}
} }
} catch (error) { } catch (error) {
setError("key", { setError("key", {

View File

@ -1,85 +1,55 @@
import { Image } from "@shared/image"; import { User } from "@app/auth/components/user";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from "@stores/constants";
import { DEFAULT_AVATAR, READONLY_RELAYS } from "@stores/constants"; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { onboardingAtom } from "@stores/onboarding"; import { updateAccount } from "@utils/storage";
import { nip02ToArray } from "@utils/transform";
import { shortenKey } from "@utils/shortenKey"; import { useContext, useState } from "react";
import { createAccount, createPleb } from "@utils/storage";
import { useAtom } from "jotai";
import { getPublicKey } from "nostr-tools";
import { useContext, useMemo, useState } from "react";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
import { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { account } = useActiveAccount();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [onboarding, setOnboarding] = useAtom(onboardingAtom); const [follows, setFollows] = useState([]);
const pubkey = useMemo(
() => (onboarding.privkey ? getPublicKey(onboarding.privkey) : ""),
[onboarding.privkey],
);
const { data, error } = useSWRSubscription( useSWRSubscription(account ? account.pubkey : null, (key: string) => {
pubkey ? pubkey : null, const unsubscribe = pool.subscribe(
(key, { next }) => { [
const unsubscribe = pool.subscribe( {
[ kinds: [3],
{ authors: [key],
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;
}
}, },
); ],
READONLY_RELAYS,
(event: any) => {
setFollows(event.tags);
},
);
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, });
);
const submit = () => { const submit = () => {
// show loading indicator // show loading indicator
setLoading(true); setLoading(true);
const follows = onboarding.follows.concat([["p", pubkey]]); // follows as list
// insert to database const followsList = nip02ToArray(follows);
createAccount(pubkey, onboarding.privkey, onboarding.metadata, follows, 1)
.then((res) => { // update account follows
if (res) { updateAccount("follows", followsList, account.pubkey);
for (const tag of onboarding.follows) {
fetch(`https://rbr.bio/${tag[1]}/metadata.json`) // redirect to home
.then((data) => data.json()) setTimeout(
.then((data) => createPleb(tag[1], data ?? "")); () => navigate("/app/prefetch", { overwriteLastHistoryEntry: true }),
} 2000,
setTimeout( );
() => navigate("/", { overwriteLastHistoryEntry: true }),
2000,
);
} else {
console.error();
}
})
.catch(console.error);
}; };
return ( return (
@ -91,8 +61,7 @@ export function Page() {
</h1> </h1>
</div> </div>
<div className="w-full rounded-lg border border-zinc-800 bg-zinc-900 p-4"> <div className="w-full rounded-lg border border-zinc-800 bg-zinc-900 p-4">
{error && <div>Failed to load profile</div>} {!account ? (
{!data ? (
<div className="w-full"> <div className="w-full">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" /> <div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
@ -104,21 +73,7 @@ export function Page() {
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center gap-2"> <User pubkey={account.pubkey} />
<Image
className="relative inline-flex h-11 w-11 rounded-lg ring-2 ring-zinc-900"
src={data.picture || DEFAULT_AVATAR}
alt={pubkey}
/>
<div>
<h3 className="font-medium leading-none text-white">
{data.display_name || data.name}
</h3>
<p className="text-base text-zinc-400">
{data.nip05 || shortenKey(pubkey)}
</p>
</div>
</div>
<button <button
type="button" type="button"
onClick={() => submit()} onClick={() => submit()}

View File

@ -12,7 +12,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { createChannel } from "@utils/storage"; import { createChannel } from "@utils/storage";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react"; import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
@ -56,7 +56,7 @@ export default function ChannelCreateModal() {
tags: [], tags: [],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish channel // publish channel
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);

View File

@ -13,7 +13,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { useResetAtom } from "jotai/utils"; import { useResetAtom } from "jotai/utils";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { useContext } from "react"; import { useContext } from "react";
export default function ChannelMessageForm({ export default function ChannelMessageForm({
@ -50,7 +50,7 @@ export default function ChannelMessageForm({
tags: tags, tags: tags,
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish note // publish note
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);

View File

@ -12,7 +12,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useState } from "react"; import { Fragment, useContext, useState } from "react";
export default function MessageHideButton({ id }: { id: string }) { export default function MessageHideButton({ id }: { id: string }) {
@ -40,7 +40,7 @@ export default function MessageHideButton({ id }: { id: string }) {
tags: [["e", id]], tags: [["e", id]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish note // publish note
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);

View File

@ -12,7 +12,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useState } from "react"; import { Fragment, useContext, useState } from "react";
export default function MessageMuteButton({ pubkey }: { pubkey: string }) { export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
@ -40,7 +40,7 @@ export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
tags: [["p", pubkey]], tags: [["p", pubkey]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish note // publish note
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);

View File

@ -12,7 +12,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getChannel } from "@utils/storage"; import { getChannel } from "@utils/storage";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react"; import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -61,7 +61,7 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
tags: [["e", id]], tags: [["e", id]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish channel // publish channel
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);

View File

@ -11,7 +11,7 @@ import useSWRSubscription from "swr/subscription";
const fetcher = async ([, id]) => { const fetcher = async ([, id]) => {
const result = await getChannel(id); const result = await getChannel(id);
if (result) { if (result) {
return JSON.parse(result.metadata); return result;
} else { } else {
return null; return null;
} }

View File

@ -38,14 +38,14 @@ export default function ChatsListItem({ pubkey }: { pubkey: string }) {
> >
<div className="relative h-5 w-5 shrink-0 rounded"> <div className="relative h-5 w-5 shrink-0 rounded">
<Image <Image
src={user.picture || DEFAULT_AVATAR} src={user?.picture || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-5 w-5 rounded bg-white object-cover" className="h-5 w-5 rounded bg-white object-cover"
/> />
</div> </div>
<div> <div>
<h5 className="max-w-[9rem] truncate font-medium text-zinc-200 group-hover:text-white"> <h5 className="max-w-[9rem] truncate font-medium text-zinc-200 group-hover:text-white">
{user.nip05 || user.name || shortenKey(pubkey)} {user?.nip05 || user.name || shortenKey(pubkey)}
</h5> </h5>
</div> </div>
</a> </a>

View File

@ -2,16 +2,17 @@ import ChatsListItem from "@app/chat/components/item";
import ChatsListSelfItem from "@app/chat/components/self"; import ChatsListSelfItem from "@app/chat/components/self";
import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getChats } from "@utils/storage"; import { getChatsByPubkey } from "@utils/storage";
import useSWR from "swr"; import useSWR from "swr";
const fetcher = ([, account]) => getChats(account); const fetcher = ([, pubkey]) => getChatsByPubkey(pubkey);
export default function ChatsList() { export default function ChatsList() {
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
const { data: chats, error }: any = useSWR( const { data: chats, error }: any = useSWR(
!isLoading && !isError && account ? ["chats", account] : null, !isLoading && !isError && account ? ["chats", account.pubkey] : null,
fetcher, fetcher,
); );
@ -30,8 +31,8 @@ export default function ChatsList() {
</div> </div>
</> </>
) : ( ) : (
chats.map((item: { pubkey: string }) => ( chats.map((item) => (
<ChatsListItem key={item.pubkey} pubkey={item.pubkey} /> <ChatsListItem key={item.sender_pubkey} pubkey={item.sender_pubkey} />
)) ))
)} )}
</div> </div>

View File

@ -9,7 +9,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useResetAtom } from "jotai/utils"; import { useResetAtom } from "jotai/utils";
import { getEventHash, nip04, signEvent } from "nostr-tools"; import { getEventHash, getSignature, nip04 } from "nostr-tools";
import { useCallback, useContext } from "react"; import { useCallback, useContext } from "react";
export default function ChatMessageForm({ export default function ChatMessageForm({
@ -40,7 +40,7 @@ export default function ChatMessageForm({
tags: [["p", receiverPubkey]], tags: [["p", receiverPubkey]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish note // publish note
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
// reset state // reset state

View File

@ -15,7 +15,6 @@ export default function ChatsListSelfItem() {
const pagePubkey = searchParams.pubkey; const pagePubkey = searchParams.pubkey;
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
const profile = account ? JSON.parse(account.metadata) : null;
return ( return (
<> <>
@ -39,14 +38,14 @@ export default function ChatsListSelfItem() {
> >
<div className="relative h-5 w-5 shrink-0 rounded"> <div className="relative h-5 w-5 shrink-0 rounded">
<Image <Image
src={profile?.picture || DEFAULT_AVATAR} src={account?.picture || DEFAULT_AVATAR}
alt={account.pubkey} alt={account.pubkey}
className="h-5 w-5 rounded bg-white object-cover" className="h-5 w-5 rounded bg-white object-cover"
/> />
</div> </div>
<div className="inline-flex items-baseline"> <div className="inline-flex items-baseline">
<h5 className="max-w-[9rem] truncate font-medium text-zinc-200"> <h5 className="max-w-[9rem] truncate font-medium text-zinc-200">
{profile?.nip05 || profile?.name || shortenKey(account.pubkey)} {account?.nip05 || account?.name || shortenKey(account.pubkey)}
</h5> </h5>
<span className="text-zinc-600">(you)</span> <span className="text-zinc-600">(you)</span>
</div> </div>

View File

@ -1,23 +1,15 @@
import { getActiveAccount } from "@utils/storage"; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import useSWR from "swr";
import { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
const fetcher = () => getActiveAccount();
export function Page() { export function Page() {
const { data, isLoading } = useSWR("account", fetcher, { const { account, isLoading } = useActiveAccount();
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
});
if (!isLoading && !data) { if (!isLoading && !account) {
navigate("/app/auth", { overwriteLastHistoryEntry: true }); navigate("/app/auth", { overwriteLastHistoryEntry: true });
} }
if (!isLoading && data) { if (!isLoading && account) {
navigate("/app/inital-data", { overwriteLastHistoryEntry: true }); navigate("/app/prefetch", { overwriteLastHistoryEntry: true });
} }
return ( return (

View File

@ -1,7 +1,6 @@
import NoteReply from "@app/note/components/metadata/reply"; import NoteReply from "@app/note/components/metadata/reply";
import NoteRepost from "@app/note/components/metadata/repost"; import NoteRepost from "@app/note/components/metadata/repost";
import NoteZap from "@app/note/components/metadata/zap"; import NoteZap from "@app/note/components/metadata/zap";
import ZapIcon from "@shared/icons/zap";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from "@stores/constants"; import { READONLY_RELAYS } from "@stores/constants";
import { decode } from "light-bolt11-decoder"; import { decode } from "light-bolt11-decoder";

View File

@ -7,7 +7,7 @@ import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
export default function NoteLike({ export default function NoteLike({
@ -35,7 +35,7 @@ export default function NoteLike({
pubkey: account.pubkey, pubkey: account.pubkey,
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish event to all relays // publish event to all relays
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
// update state // update state

View File

@ -10,7 +10,7 @@ import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { compactNumber } from "@utils/number"; import { compactNumber } from "@utils/number";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react"; import { Fragment, useContext, useEffect, useState } from "react";
export default function NoteReply({ export default function NoteReply({
@ -24,7 +24,6 @@ export default function NoteReply({
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const { account, isLoading, isError } = useActiveAccount(); const { account, isLoading, isError } = useActiveAccount();
const profile = account ? JSON.parse(account.metadata) : null;
const closeModal = () => { const closeModal = () => {
setIsOpen(false); setIsOpen(false);
@ -44,7 +43,7 @@ export default function NoteReply({
tags: [["e", id]], tags: [["e", id]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish event // publish event
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
@ -106,7 +105,7 @@ export default function NoteReply({
<div> <div>
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10"> <div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
<Image <Image
src={profile?.picture} src={account?.picture}
alt="user's avatar" alt="user's avatar"
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />

View File

@ -8,7 +8,7 @@ import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { compactNumber } from "@utils/number"; import { compactNumber } from "@utils/number";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
export default function NoteRepost({ export default function NoteRepost({
@ -36,7 +36,7 @@ export default function NoteRepost({
pubkey: account.pubkey, pubkey: account.pubkey,
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish event to all relays // publish event to all relays
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
// update state // update state

View File

@ -6,15 +6,14 @@ import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount"; import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, signEvent } from "nostr-tools"; import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
export default function NoteReplyForm({ id }: { id: string }) { export default function NoteReplyForm({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount();
const { account, isLoading, isError } = useActiveAccount();
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const profile = account ? JSON.parse(account.metadata) : null;
const submitEvent = () => { const submitEvent = () => {
if (!isLoading && !isError && account) { if (!isLoading && !isError && account) {
@ -26,7 +25,7 @@ export default function NoteReplyForm({ id }: { id: string }) {
tags: [["e", id]], tags: [["e", id]],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey); event.sig = getSignature(event, account.privkey);
// publish note // publish note
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
@ -42,7 +41,7 @@ export default function NoteReplyForm({ id }: { id: string }) {
<div> <div>
<div className="relative h-9 w-9 shrink-0 overflow-hidden rounded-md"> <div className="relative h-9 w-9 shrink-0 overflow-hidden rounded-md">
<Image <Image
src={profile?.picture} src={account?.picture}
alt={account?.pubkey} alt={account?.pubkey}
className="h-9 w-9 rounded-md object-cover" className="h-9 w-9 rounded-md object-cover"
/> />

View File

@ -1,23 +1,18 @@
import { RelayContext } from "@shared/relayProvider";
import LumeIcon from "@icons/lume"; import LumeIcon from "@icons/lume";
import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from "@stores/constants"; import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from "@utils/date"; import { dateToUnix, getHourAgo } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { import {
addToBlacklist, addToBlacklist,
countTotalLongNotes,
countTotalNotes, countTotalNotes,
createChat, createChat,
createNote, createNote,
getActiveAccount,
getLastLogin, getLastLogin,
updateLastLogin,
} from "@utils/storage"; } from "@utils/storage";
import { getParentID, nip02ToArray } from "@utils/transform"; import { getParentID } from "@utils/transform";
import { useCallback, useContext, useRef } from "react";
import { useContext, useEffect, useRef } from "react"; import useSWRSubscription from "swr/subscription";
import { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
function isJSON(str: string) { function isJSON(str: string) {
@ -29,79 +24,68 @@ function isJSON(str: string) {
return true; return true;
} }
let lastLogin: string;
let totalNotes: number;
if (typeof window !== "undefined") {
lastLogin = await getLastLogin();
totalNotes = await countTotalNotes();
}
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const now = useRef(new Date()); const now = useRef(new Date());
const eose = useRef(0);
useEffect(() => { const { account, isLoading, isError } = useActiveAccount();
let unsubscribe: () => void;
let timeout: any;
const fetchInitalData = async () => { const getQuery = useCallback(() => {
const account = await getActiveAccount(); const query = [];
const lastLogin = await getLastLogin(); const follows = JSON.parse(account.follows);
const notes = await countTotalNotes();
const longNotes = await countTotalLongNotes();
const follows = nip02ToArray(JSON.parse(account.follows)); let queryNoteSince: number;
const query = []; let querySince: number;
let sinceNotes: number; if (totalNotes === 0) {
let sinceLongNotes: number; queryNoteSince = dateToUnix(getHourAgo(48, now.current));
} else {
if (notes === 0) { if (parseInt(lastLogin) > 0) {
sinceNotes = dateToUnix(getHourAgo(48, now.current)); queryNoteSince = parseInt(lastLogin);
} else { } else {
if (parseInt(lastLogin) > 0) { queryNoteSince = dateToUnix(getHourAgo(48, now.current));
sinceNotes = parseInt(lastLogin);
} else {
sinceNotes = dateToUnix(getHourAgo(48, now.current));
}
} }
}
if (longNotes === 0) { // kind 1 (notes) query
sinceLongNotes = 0; query.push({
} else { kinds: [1, 6, 1063],
if (parseInt(lastLogin) > 0) { authors: follows,
sinceLongNotes = parseInt(lastLogin); since: queryNoteSince,
} else { });
sinceLongNotes = 0;
}
}
// kind 1 (notes) query // kind 4 (chats) query
query.push({ query.push({
kinds: [1, 6, 1063], kinds: [4],
authors: follows, "#p": [account.pubkey],
since: sinceNotes, since: querySince,
until: dateToUnix(now.current), });
});
// kind 4 (chats) query // kind 43, 43 (mute user, hide message) query
query.push({ query.push({
kinds: [4], authors: [account.pubkey],
"#p": [account.pubkey], kinds: [43, 44],
since: 0, since: querySince,
until: dateToUnix(now.current), });
});
// kind 43, 43 (mute user, hide message) query return query;
query.push({ }, [account.follows]);
authors: [account.pubkey],
kinds: [43, 44],
since: 0,
until: dateToUnix(now.current),
});
// kind 30023 (long post) query useSWRSubscription(
query.push({ !isLoading && !isError && account ? "prefetch" : null,
kinds: [30023], () => {
since: sinceLongNotes, const query = getQuery();
until: dateToUnix(now.current), const unsubscribe = pool.subscribe(
});
// subscribe relays
unsubscribe = pool.subscribe(
query, query,
READONLY_RELAYS, READONLY_RELAYS,
(event: any) => { (event: any) => {
@ -124,9 +108,13 @@ export function Page() {
} }
// chat // chat
case 4: case 4:
if (event.pubkey !== account.pubkey) { createChat(
createChat(account.id, event.pubkey, event.created_at); event.id,
} account.pubkey,
event.pubkey,
event.content,
event.created_at,
);
break; break;
// repost // repost
case 6: case 6:
@ -165,47 +153,24 @@ export function Page() {
"", "",
); );
break; 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: default:
break; break;
} }
}, },
undefined, undefined,
() => { () => {
updateLastLogin(dateToUnix(now.current)); eose.current += 1;
timeout = setTimeout(() => { if (eose.current === READONLY_RELAYS.length) {
navigate("/app/space", { overwriteLastHistoryEntry: true }); navigate("/app/space", { overwriteLastHistoryEntry: true });
}, 5000); }
}, },
); );
};
fetchInitalData().catch(console.error); return () => {
return () => {
if (unsubscribe) {
unsubscribe(); unsubscribe();
} };
clearTimeout(timeout); },
}; );
}, [pool]);
return ( return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white"> <div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">

View File

@ -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 "./index.css";
import { Shell } from "./shell"; import { Shell } from "./shell";
import { PageContextClient } from "./types"; 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 clientRouting = true;
export const hydrationCanBeAborted = true; export const hydrationCanBeAborted = true;

View File

@ -1,10 +1,9 @@
import { Shell } from "./shell";
import { PageContextServer } from "./types";
import { StrictMode } from "react"; import { StrictMode } from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
import { dangerouslySkipEscape, escapeInject } from "vite-plugin-ssr/server"; import { dangerouslySkipEscape, escapeInject } from "vite-plugin-ssr/server";
import { Shell } from "./shell";
import { PageContextServer } from "./types";
export const passToClient = ["pageProps"]; export const passToClient = ["pageProps"];
export function render(pageContext: PageContextServer) { export function render(pageContext: PageContextServer) {

View File

@ -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 { LayoutDefault } from "./layoutDefault";
import { PageContext } from "./types"; 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(); const queryClient = new QueryClient();

View File

@ -1,15 +1,16 @@
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
export default function ActiveAccount({ user }: { user: any }) { export default function ActiveAccount({ data }: { data: any }) {
const userData = JSON.parse(user.metadata); const { user } = useProfile(data.npub);
return ( return (
<button type="button" className="relative h-11 w-11 overflow-hidden"> <button type="button" className="relative h-11 w-11 overflow-hidden">
<Image <Image
src={userData.picture || DEFAULT_AVATAR} src={user?.picture || DEFAULT_AVATAR}
alt="user's avatar" alt={data.npub}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />
</button> </button>

View File

@ -1,15 +1,16 @@
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
export default function InactiveAccount({ user }: { user: any }) { export default function InactiveAccount({ data }: { data: any }) {
const userData = JSON.parse(user.metadata); const { user } = useProfile(data.npub);
return ( return (
<div className="relative h-11 w-11 shrink rounded-md"> <div className="relative h-11 w-11 shrink rounded-md">
<Image <Image
src={userData.picture || DEFAULT_AVATAR} src={user?.picture || DEFAULT_AVATAR}
alt="user's avatar" alt={data.npub}
className="h-11 w-11 rounded-lg object-cover" className="h-11 w-11 rounded-lg object-cover"
/> />
</div> </div>

View File

@ -6,7 +6,7 @@ import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; 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 { useCallback, useContext, useMemo, useState } from "react";
import { Node, Transforms, createEditor } from "slate"; import { Node, Transforms, createEditor } from "slate";
import { withHistory } from "slate-history"; import { withHistory } from "slate-history";
@ -91,7 +91,7 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
tags: [], tags: [],
}; };
event.id = getEventHash(event); event.id = getEventHash(event);
event.sig = signEvent(event, privkey); event.sig = getSignature(event, privkey);
// publish note // publish note
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);

View File

@ -44,7 +44,7 @@ export default function EventCollector() {
since: dateToUnix(now.current), since: dateToUnix(now.current),
}, },
{ {
kinds: [0, 3], kinds: [3],
authors: [key.pubkey], authors: [key.pubkey],
}, },
{ {
@ -60,10 +60,6 @@ export default function EventCollector() {
READONLY_RELAYS, READONLY_RELAYS,
(event: any) => { (event: any) => {
switch (event.kind) { switch (event.kind) {
// metadata
case 0:
updateAccount("metadata", event.content, event.pubkey);
break;
// short text note // short text note
case 1: { case 1: {
const parentID = getParentID(event.tags, event.id); const parentID = getParentID(event.tags, event.id);
@ -82,15 +78,21 @@ export default function EventCollector() {
break; break;
} }
// contacts // contacts
case 3: case 3: {
const follows = nip02ToArray(event.tags);
// update account's folllows with NIP-02 tag list // update account's folllows with NIP-02 tag list
updateAccount("follows", event.tags, event.pubkey); updateAccount("follows", follows, event.pubkey);
break; break;
}
// chat // chat
case 4: case 4:
if (event.pubkey !== key.pubkey) { createChat(
createChat(key.id, event.pubkey, event.created_at); event.id,
} key.pubkey,
event.pubkey,
event.content,
event.created_at,
);
break; break;
// repost // repost
case 6: case 6:

View File

@ -25,7 +25,7 @@ export default function MultiAccounts() {
{!activeAccount ? ( {!activeAccount ? (
<div className="group relative flex h-10 w-10 shrink animate-pulse items-center justify-center rounded-lg bg-zinc-900" /> <div className="group relative flex h-10 w-10 shrink animate-pulse items-center justify-center rounded-lg bg-zinc-900" />
) : ( ) : (
<ActiveAccount user={activeAccount} /> <ActiveAccount data={activeAccount} />
)} )}
</> </>
<div> <div>
@ -49,7 +49,7 @@ export default function MultiAccounts() {
) : ( ) : (
accounts.map( accounts.map(
(account: { is_active: number; pubkey: string }) => ( (account: { is_active: number; pubkey: string }) => (
<InactiveAccount key={account.pubkey} user={account} /> <InactiveAccount key={account.pubkey} data={account} />
), ),
) )
)} )}

View File

@ -1,5 +1,4 @@
import { FULL_RELAYS } from "@stores/constants"; import { FULL_RELAYS } from "@stores/constants";
import { RelayPool } from "nostr-relaypool"; import { RelayPool } from "nostr-relaypool";
import { createContext } from "react"; import { createContext } from "react";

View File

@ -6,12 +6,12 @@ export const DEFAULT_CHANNEL_BANNER =
"https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg"; "https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg";
// metadata service // metadata service
export const METADATA_SERVICE = "https://rbr.bio"; export const METADATA_SERVICE = "https://us.rbr.bio";
// read-only relay list // read-only relay list
export const READONLY_RELAYS = [ export const READONLY_RELAYS = [
"wss://welcome.nostr.wine", "wss://welcome.nostr.wine",
"wss://relay.nostr.band/all", "wss://relay.nostr.band",
]; ];
// write-only relay list // write-only relay list
@ -23,6 +23,6 @@ export const WRITEONLY_RELAYS = [
// full-relay list // full-relay list
export const FULL_RELAYS = [ export const FULL_RELAYS = [
"wss://welcome.nostr.wine", "wss://welcome.nostr.wine",
"wss://relay.nostr.band/all", "wss://relay.nostr.band",
"wss://nostr.mutinywallet.com", "wss://nostr.mutinywallet.com",
]; ];

View File

@ -1,5 +1,4 @@
import { getActiveAccount } from "@utils/storage"; import { getActiveAccount } from "@utils/storage";
import useSWR from "swr"; import useSWR from "swr";
const fetcher = () => getActiveAccount(); const fetcher = () => getActiveAccount();

View File

@ -1,33 +1,47 @@
import { METADATA_SERVICE } from "@stores/constants"; import { METADATA_SERVICE } from "@stores/constants";
import { createPleb, getPleb } from "@utils/storage"; import { createPleb, getPleb } from "@utils/storage";
import { nip19 } from "nostr-tools";
import useSWR from "swr"; import useSWR from "swr";
const fetcher = async (pubkey: string) => { const fetcher = async (key: string) => {
const result = await getPleb(pubkey); let npub: string;
if (result) {
const metadata = JSON.parse(result["metadata"]);
result["content"] = metadata.content;
result["metadata"] = undefined;
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; return result;
} else { } else {
const result = await fetch(`${METADATA_SERVICE}/${pubkey}/metadata.json`); const res = await fetch(`${METADATA_SERVICE}/${key}/metadata.json`);
const resultJSON = await result.json();
const cache = await createPleb(pubkey, resultJSON);
if (cache) { if (!res.ok) {
return resultJSON; 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) { export function useProfile(key: string) {
const { data, error, isLoading } = useSWR(pubkey, fetcher); const { data, error, isLoading } = useSWR(key, fetcher, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: true,
});
return { return {
user: data ? JSON.parse(data.content ? data.content : null) : null, user: data,
isLoading, isLoading,
isError: error, isError: error,
}; };

View File

@ -1,3 +1,4 @@
import { nip19 } from "nostr-tools";
import Database from "tauri-plugin-sql-api"; import Database from "tauri-plugin-sql-api";
let db: null | Database = null; let db: null | Database = null;
@ -15,10 +16,7 @@ export async function connect(): Promise<Database> {
// get active account // get active account
export async function getActiveAccount() { export async function getActiveAccount() {
const db = await connect(); const db = await connect();
// #TODO: check is_active == true const result = await db.select("SELECT * FROM accounts WHERE is_active = 1;");
const result = await db.select(
"SELECT * FROM accounts WHERE is_active = 1 LIMIT 1;",
);
return result[0]; return result[0];
} }
@ -32,16 +30,16 @@ export async function getAccounts() {
// create account // create account
export async function createAccount( export async function createAccount(
npub: string,
pubkey: string, pubkey: string,
privkey: string, privkey: string,
metadata: string,
follows?: string[][], follows?: string[][],
is_active?: number, is_active?: number,
) { ) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(
"INSERT OR IGNORE INTO accounts (pubkey, privkey, metadata, follows, is_active) VALUES (?, ?, ?, ?, ?);", "INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);",
[pubkey, privkey, metadata, follows || "", is_active || 0], [npub, pubkey, privkey, follows || "", is_active || 0],
); );
} }
@ -65,20 +63,47 @@ export async function getPlebs() {
} }
// get pleb by pubkey // get pleb by pubkey
export async function getPleb(pubkey: string) { export async function getPleb(npub: string) {
const db = await connect(); const db = await connect();
const result = await db.select( const result = await db.select(`SELECT * FROM plebs WHERE npub = "${npub}";`);
`SELECT * FROM plebs WHERE pubkey = "${pubkey}"`,
); if (result) {
return result[0]; return result[0];
} else {
return null;
}
} }
// create pleb // create pleb
export async function createPleb(pubkey: string, metadata: string) { export async function createPleb(key: string, json: any) {
const db = await connect(); 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( return await db.execute(
"INSERT OR IGNORE INTO plebs (pubkey, metadata) VALUES (?, ?);", "INSERT OR REPLACE INTO plebs (npub, display_name, name, username, about, bio, website, picture, banner, nip05, lud06, lud16, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
[pubkey, metadata], [
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 // update channel metadata
export async function updateChannelMetadata(event_id: string, value: string) { export async function updateChannelMetadata(event_id: string, value: string) {
const db = await connect(); const db = await connect();
const data = JSON.parse(value);
return await db.execute( return await db.execute(
"UPDATE channels SET metadata = ? WHERE event_id = ?;", "UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;",
[value, event_id], [data.name, data.picture, data.about, event_id],
); );
} }
// get all chats // get all chats
export async function getChats(account_id: number) { export async function getChatsByPubkey(pubkey: string) {
const db = await connect(); const db = await connect();
return await db.select( 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 // create chat
export async function createChat( export async function createChat(
account_id: number, event_id: string,
pubkey: string, receiver_pubkey: string,
sender_pubkey: string,
content: string,
created_at: number, created_at: number,
) { ) {
const db = await connect(); const db = await connect();
return await db.execute( return await db.execute(
"INSERT OR IGNORE INTO chats (account_id, pubkey, created_at) VALUES (?, ?, ?);", "INSERT OR IGNORE INTO chats (event_id, receiver_pubkey, sender_pubkey, content, created_at) VALUES (?, ?, ?, ?, ?);",
[account_id, pubkey, created_at], [event_id, receiver_pubkey, sender_pubkey, content, created_at],
); );
} }