migrate to ndk

This commit is contained in:
Ren Amamiya 2023-06-08 18:09:36 +07:00
parent 75a33d205a
commit 0ba9877785
53 changed files with 2749 additions and 930 deletions

View File

@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"@floating-ui/react": "^0.23.1", "@floating-ui/react": "^0.23.1",
"@headlessui/react": "^1.7.15", "@headlessui/react": "^1.7.15",
"@nostr-dev-kit/ndk": "^0.4.4",
"@tanstack/react-virtual": "3.0.0-beta.54", "@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.3.0", "@tauri-apps/api": "^1.3.0",
"@vidstack/react": "^0.4.5", "@vidstack/react": "^0.4.5",
@ -29,7 +30,7 @@
"react-hook-form": "^7.44.3", "react-hook-form": "^7.44.3",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-resizable-panels": "^0.0.48", "react-resizable-panels": "^0.0.48",
"react-virtuoso": "^4.3.8", "react-virtuoso": "^4.3.9",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"slate": "^0.94.1", "slate": "^0.94.1",
"slate-history": "^0.93.0", "slate-history": "^0.93.0",
@ -44,7 +45,7 @@
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "^1.3.1", "@tauri-apps/cli": "^1.3.1",
"@types/node": "^18.16.16", "@types/node": "^18.16.16",
"@types/react": "^18.2.8", "@types/react": "^18.2.9",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
"@types/youtube-player": "^5.5.7", "@types/youtube-player": "^5.5.7",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",

File diff suppressed because it is too large Load Diff

View File

@ -19,17 +19,16 @@ CREATE TABLE
plebs ( plebs (
id INTEGER NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
npub TEXT NOT NULL UNIQUE, npub TEXT NOT NULL UNIQUE,
display_name TEXT,
name TEXT, name TEXT,
username TEXT, displayName TEXT,
about TEXT, image TEXT,
bio TEXT,
website TEXT,
picture TEXT,
banner TEXT, banner TEXT,
bio TEXT,
nip05 TEXT, nip05 TEXT,
lud06 TEXT, lud06 TEXT,
lud16 TEXT, lud16 TEXT,
about TEXT,
zapService TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );

View File

@ -10,7 +10,7 @@ export function User({ pubkey }: { pubkey: string }) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative h-11 w-11 shrink rounded-md"> <div className="relative h-11 w-11 shrink rounded-md">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
decoding="async" decoding="async"
@ -18,7 +18,7 @@ export function User({ pubkey }: { pubkey: string }) {
</div> </div>
<div className="flex w-full flex-1 flex-col items-start text-start"> <div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-white"> <span className="truncate font-medium leading-tight text-white">
{user?.display_name || user?.name} {user?.displayName || user?.name}
</span> </span>
<span className="text-base leading-tight text-zinc-400"> <span className="text-base leading-tight text-zinc-400">
{user?.nip05?.toLowerCase() || shortenKey(pubkey)} {user?.nip05?.toLowerCase() || shortenKey(pubkey)}

View File

@ -1,15 +1,15 @@
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
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 { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react"; import { useContext, 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 ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const [image, setImage] = useState(DEFAULT_AVATAR); const [image, setImage] = useState(DEFAULT_AVATAR);
@ -25,28 +25,30 @@ export function Page() {
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
setLoading(true); setLoading(true);
const event: any = { try {
content: JSON.stringify(data), const signer = new NDKPrivateKeySigner(account.privkey);
created_at: Math.floor(Date.now() / 1000), ndk.signer = signer;
kind: 0,
pubkey: account.pubkey,
tags: [],
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = JSON.stringify(data);
event.kind = 0;
event.pubkey = account.pubkey;
event.tags = [];
// publish event
event.publish();
// publish // redirect to step 3
pool.publish(event, WRITEONLY_RELAYS); setTimeout(
() =>
// redirect to step 3 navigate("/app/auth/create/step-3", {
setTimeout( overwriteLastHistoryEntry: true,
() => }),
navigate("/app/auth/create/step-3", { 2000,
overwriteLastHistoryEntry: true, );
}), } catch {
2000, console.log("error");
); }
}; };
useEffect(() => { useEffect(() => {
@ -94,7 +96,7 @@ export function Page() {
<div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20"> <div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
<input <input
type={"text"} type={"text"}
{...register("display_name", { {...register("displayName", {
required: true, required: true,
minLength: 4, minLength: 4,
})} })}
@ -105,11 +107,11 @@ export function Page() {
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-base font-semibold uppercase tracking-wider text-zinc-400"> <label className="text-base font-semibold uppercase tracking-wider text-zinc-400">
About Bio
</label> </label>
<div className="relative h-20 w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20"> <div className="relative h-20 w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
<textarea <textarea
{...register("about")} {...register("bio")}
spellCheck={false} spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
/> />
@ -119,7 +121,7 @@ export function Page() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} disabled={!isDirty || !isValid}
className="w-full transform rounded-lg bg-fuchsia-500 px-3.5 py-2.5 font-medium text-white shadow-button hover:bg-fuchsia-600 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-70" className="inline-flex h-10 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 px-3.5 font-medium text-white shadow-button hover:bg-fuchsia-600 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-70"
> >
{loading ? ( {loading ? (
<svg <svg

View File

@ -1,10 +1,9 @@
import { User } from "@app/auth/components/user"; import { User } from "@app/auth/components/user";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CheckCircleIcon } from "@shared/icons"; import { CheckCircleIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { arrayToNIP02 } from "@utils/transform"; import { arrayToNIP02 } from "@utils/transform";
import { getEventHash, getSignature } 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";
@ -108,7 +107,7 @@ const initialList = [
]; ];
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const [account, updateFollows] = useActiveAccount((state: any) => [ const [account, updateFollows] = useActiveAccount((state: any) => [
state.account, state.account,
@ -129,33 +128,34 @@ export function Page() {
const submit = async () => { const submit = async () => {
setLoading(true); setLoading(true);
// update account follows try {
updateFollows(follows); const tags = arrayToNIP02(follows);
const signer = new NDKPrivateKeySigner(account.privkey);
ndk.signer = signer;
const tags = arrayToNIP02(follows); const event = new NDKEvent(ndk);
// build event
event.content = "";
event.kind = 3;
event.pubkey = account.pubkey;
event.tags = tags;
// publish event
event.publish();
const event: any = { // update account follows
content: "", updateFollows(follows);
created_at: Math.floor(Date.now() / 1000),
kind: 3,
pubkey: account.pubkey,
tags: tags,
};
event.id = getEventHash(event); // redirect to step 3
event.sig = getSignature(event, account.privkey); setTimeout(
() =>
// publish navigate("/", {
pool.publish(event, WRITEONLY_RELAYS); overwriteLastHistoryEntry: true,
}),
// redirect to step 3 2000,
setTimeout( );
() => } catch {
navigate("/", { console.log("error");
overwriteLastHistoryEntry: true, }
}),
2000,
);
}; };
return ( return (

View File

@ -1,53 +1,41 @@
import { User } from "@app/auth/components/user"; import { User } from "@app/auth/components/user";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { METADATA_RELAY } from "@stores/constants"; import { setToArray } from "@utils/transform";
import { nip02ToArray } from "@utils/transform";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
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 ndk = useContext(RelayContext);
const [loading, setLoading] = useState(false);
const [account, updateFollows] = useActiveAccount((state: any) => [ const [account, updateFollows] = useActiveAccount((state: any) => [
state.account, state.account,
state.updateFollows, state.updateFollows,
]); ]);
const [loading, setLoading] = useState(false);
const [follows, setFollows] = useState(null);
useSWRSubscription(account ? ["follows", account.pubkey] : null, () => { const submit = async () => {
const unsubscribe = pool.subscribe(
[
{
kinds: [3],
authors: [account.pubkey],
},
],
METADATA_RELAY,
(event: any) => {
setFollows(event.tags);
},
);
return () => {
unsubscribe();
};
});
const submit = () => {
// show loading indicator // show loading indicator
setLoading(true); setLoading(true);
// follows as list try {
const followsList = nip02ToArray(follows); const user = ndk.getUser({ hexpubkey: account.pubkey });
const follows = await user.follows();
// update account follows in store // follows as list
updateFollows(followsList); const followsList = setToArray(follows);
// redirect to home // update account follows in store
setTimeout(() => navigate("/", { overwriteLastHistoryEntry: true }), 2000); updateFollows(followsList);
// redirect to home
setTimeout(
() => navigate("/", { overwriteLastHistoryEntry: true }),
2000,
);
} catch {
console.log("error");
}
}; };
return ( return (

View File

@ -1,19 +1,19 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { AvatarUploader } from "@shared/avatarUploader"; import { AvatarUploader } from "@shared/avatarUploader";
import { CancelIcon, PlusIcon } from "@shared/icons"; import { CancelIcon, PlusIcon } from "@shared/icons";
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { createChannel } from "@utils/storage"; import { createChannel } from "@utils/storage";
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 { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
export function ChannelCreateModal() { export function ChannelCreateModal() {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -39,20 +39,20 @@ export function ChannelCreateModal() {
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
setLoading(true); setLoading(true);
if (account) { try {
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: JSON.stringify(data), ndk.signer = signer;
created_at: dateToUnix(),
kind: 40,
pubkey: account.pubkey,
tags: [],
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = JSON.stringify(data);
event.kind = 40;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [];
// publish channel // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// insert to database // insert to database
createChannel(event.id, event.pubkey, event.content, event.created_at); createChannel(event.id, event.pubkey, event.content, event.created_at);
@ -65,9 +65,9 @@ export function ChannelCreateModal() {
setIsOpen(false); setIsOpen(false);
// redirect to channel page // redirect to channel page
navigate(`/app/channel?id=${event.id}`); navigate(`/app/channel?id=${event.id}`);
}, 2000); }, 1000);
} else { } catch (e) {
console.log("error"); console.log("error: ", e);
} }
}; };

View File

@ -12,7 +12,7 @@ export function Member({ pubkey }: { pubkey: string }) {
) : ( ) : (
<Image <Image
className="inline-block h-8 w-8 rounded-md bg-white ring-2 ring-zinc-950 transition-all duration-150 ease-in-out" className="inline-block h-8 w-8 rounded-md bg-white ring-2 ring-zinc-950 transition-all duration-150 ease-in-out"
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={user?.pubkey || "user avatar"} alt={user?.pubkey || "user avatar"}
/> />
)} )}

View File

@ -1,15 +1,14 @@
import { UserReply } from "@app/channel/components/messages/userReply"; import { UserReply } from "@app/channel/components/messages/userReply";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CancelIcon } from "@shared/icons"; import { CancelIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { useChannelMessages } from "@stores/channels"; import { useChannelMessages } from "@stores/channels";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
export function ChannelMessageForm({ channelID }: { channelID: string }) { export function ChannelMessageForm({ channelID }: { channelID: string }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
@ -31,19 +30,20 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
tags = [["e", channelID, "", "root"]]; tags = [["e", channelID, "", "root"]];
} }
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: value, ndk.signer = signer;
created_at: dateToUnix(),
kind: 42,
pubkey: account.pubkey,
tags: tags,
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = value;
event.kind = 42;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = tags;
// publish event
event.publish();
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset state // reset state
setValue(""); setValue("");
}; };

View File

@ -1,16 +1,15 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CancelIcon, HideIcon } from "@shared/icons"; import { CancelIcon, HideIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip"; import { Tooltip } from "@shared/tooltip";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { useChannelMessages } from "@stores/channels"; import { useChannelMessages } from "@stores/channels";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useState } from "react"; import { Fragment, useContext, useState } from "react";
export function MessageHideButton({ id }: { id: string }) { export function MessageHideButton({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const hide = useChannelMessages((state: any) => state.hideMessage); const hide = useChannelMessages((state: any) => state.hideMessage);
@ -25,19 +24,19 @@ export function MessageHideButton({ id }: { id: string }) {
}; };
const hideMessage = () => { const hideMessage = () => {
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: "", ndk.signer = signer;
created_at: dateToUnix(),
kind: 43,
pubkey: account.pubkey,
tags: [["e", id]],
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = "";
event.kind = 43;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [["e", id]];
// publish note // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// update state // update state
hide(id); hide(id);

View File

@ -1,16 +1,15 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CancelIcon, MuteIcon } from "@shared/icons"; import { CancelIcon, MuteIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip"; import { Tooltip } from "@shared/tooltip";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { useChannelMessages } from "@stores/channels"; import { useChannelMessages } from "@stores/channels";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useState } from "react"; import { Fragment, useContext, useState } from "react";
export function MessageMuteButton({ pubkey }: { pubkey: string }) { export function MessageMuteButton({ pubkey }: { pubkey: string }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const mute = useChannelMessages((state: any) => state.muteUser); const mute = useChannelMessages((state: any) => state.muteUser);
@ -25,19 +24,19 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
}; };
const muteUser = () => { const muteUser = () => {
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: "", ndk.signer = signer;
created_at: dateToUnix(),
kind: 44,
pubkey: account.pubkey,
tags: [["p", pubkey]],
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = "";
event.kind = 44;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [["p", pubkey]];
// publish note // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// update state // update state
mute(pubkey); mute(pubkey);

View File

@ -31,7 +31,7 @@ export function ChannelMessageUser({
<> <>
<div className="relative h-11 w-11 shrink-0 rounded-md"> <div className="relative h-11 w-11 shrink-0 rounded-md">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />

View File

@ -24,7 +24,7 @@ export function ChannelMessageUserMute({
<> <>
<div className="relative h-11 w-11 shrink-0 rounded-md"> <div className="relative h-11 w-11 shrink-0 rounded-md">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />

View File

@ -17,7 +17,7 @@ export function UserReply({ pubkey }: { pubkey: string }) {
<> <>
<div className="relative h-9 w-9 shrink overflow-hidden rounded"> <div className="relative h-9 w-9 shrink overflow-hidden rounded">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-9 w-9 rounded object-cover" className="h-9 w-9 rounded object-cover"
/> />

View File

@ -22,7 +22,7 @@ export function ChannelMetadata({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="relative shrink-0 rounded-md h-11 w-11"> <div className="relative shrink-0 rounded-md h-11 w-11">
<Image <Image
src={metadata?.picture || DEFAULT_AVATAR} src={metadata?.image || DEFAULT_AVATAR}
alt={id} alt={id}
className="h-11 w-11 rounded-md object-contain bg-zinc-900" className="h-11 w-11 rounded-md object-contain bg-zinc-900"
/> />

View File

@ -41,14 +41,14 @@ export function MutedItem({ data }: { data: any }) {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="relative h-9 w-9 shrink rounded-md"> <div className="relative h-9 w-9 shrink rounded-md">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={data.content} alt={data.content}
className="h-9 w-9 rounded-md object-cover" className="h-9 w-9 rounded-md object-cover"
/> />
</div> </div>
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start"> <div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
<span className="truncate text-base font-medium leading-none text-white"> <span className="truncate text-base font-medium leading-none text-white">
{user?.display_name || user?.name || "Pleb"} {user?.displayName || user?.name || "Pleb"}
</span> </span>
<span className="text-base leading-none text-zinc-400"> <span className="text-base leading-none text-zinc-400">
{shortenKey(data.content)} {shortenKey(data.content)}

View File

@ -1,18 +1,18 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { AvatarUploader } from "@shared/avatarUploader"; import { AvatarUploader } from "@shared/avatarUploader";
import { CancelIcon, EditIcon } from "@shared/icons"; import { CancelIcon, EditIcon } from "@shared/icons";
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { getChannel } from "@utils/storage"; import { getChannel } from "@utils/storage";
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";
export function ChannelUpdateModal({ id }: { id: string }) { export function ChannelUpdateModal({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -38,7 +38,7 @@ export function ChannelUpdateModal({ id }: { id: string }) {
const channel = await getChannel(id); const channel = await getChannel(id);
const metadata = JSON.parse(channel.metadata); const metadata = JSON.parse(channel.metadata);
// update image state // update image state
setImage(metadata.picture); setImage(metadata.image);
// set default values // set default values
return metadata; return metadata;
}, },
@ -47,28 +47,28 @@ export function ChannelUpdateModal({ id }: { id: string }) {
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
setLoading(true); setLoading(true);
if (account) { try {
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: JSON.stringify(data), ndk.signer = signer;
created_at: dateToUnix(),
kind: 41,
pubkey: account.pubkey,
tags: [["e", id]],
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = JSON.stringify(data);
event.kind = 41;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [["e", id]];
// publish channel // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// reset form // reset form
reset(); reset();
// close modal // close modal
setIsOpen(false); setIsOpen(false);
setLoading(false); } catch (e) {
} else { console.log("error: ", e);
console.log("error");
} }
}; };

View File

@ -7,7 +7,6 @@ import { ChannelUpdateModal } from "@app/channel/components/updateModal";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { useChannelMessages } from "@stores/channels"; import { useChannelMessages } from "@stores/channels";
import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from "@utils/date"; import { dateToUnix, getHourAgo } from "@utils/date";
import { usePageContext } from "@utils/hooks/usePageContext"; import { usePageContext } from "@utils/hooks/usePageContext";
import { getActiveBlacklist, getBlacklist } from "@utils/storage"; import { getActiveBlacklist, getBlacklist } from "@utils/storage";
@ -29,7 +28,7 @@ const fetchHided = async ([, id]) => {
}; };
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const pageContext = usePageContext(); const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search; const searchParams: any = pageContext.urlParsed.search;
@ -57,40 +56,36 @@ export function Page() {
account && channelID && muted && hided ? ["channel", channelID] : null, account && channelID && muted && hided ? ["channel", channelID] : null,
() => { () => {
// subscribe to channel // subscribe to channel
const unsubscribe = pool.subscribe( const sub = ndk.subscribe({
[ "#e": [channelID],
{ kinds: [42],
"#e": [channelID], since: dateToUnix(getHourAgo(24, now.current)),
kinds: [42], limit: 20,
since: dateToUnix(getHourAgo(24, now.current)), });
limit: 20,
},
],
READONLY_RELAYS,
(event: { id: string; pubkey: string }) => {
const message: any = event;
// handle hide message sub.addListener("event", (event: { id: string; pubkey: string }) => {
if (hided.includes(event.id)) { const message: any = event;
message["hide"] = true;
} else {
message["hide"] = false;
}
// handle mute user // handle hide message
if (muted.array.includes(event.pubkey)) { if (hided.includes(event.id)) {
message["mute"] = true; message["hide"] = true;
} else { } else {
message["mute"] = false; message["hide"] = false;
} }
// add to store // handle mute user
addMessage(message); if (muted.array.includes(event.pubkey)) {
}, message["mute"] = true;
); } else {
message["mute"] = false;
}
// add to store
addMessage(message);
});
return () => { return () => {
unsubscribe(); sub.stop();
clear(); clear();
}; };
}, },

View File

@ -34,7 +34,7 @@ export function ChatsListItem({ data }: { data: any }) {
> >
<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?.image || DEFAULT_AVATAR}
alt={data.sender_pubkey} alt={data.sender_pubkey}
className="h-5 w-5 rounded bg-white object-cover" className="h-5 w-5 rounded bg-white object-cover"
/> />
@ -42,7 +42,9 @@ export function ChatsListItem({ data }: { data: any }) {
<div className="w-full inline-flex items-center justify-between"> <div className="w-full inline-flex items-center justify-between">
<div className="inline-flex items-baseline gap-1"> <div className="inline-flex items-baseline gap-1">
<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(data.sender_pubkey)} {user?.nip05 ||
user?.displayName ||
shortenKey(data.sender_pubkey)}
</h5> </h5>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">

View File

@ -1,10 +1,10 @@
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { EnterIcon } from "@shared/icons"; import { EnterIcon } from "@shared/icons";
import { MediaUploader } from "@shared/mediaUploader"; import { MediaUploader } from "@shared/mediaUploader";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useChatMessages } from "@stores/chats"; import { useChatMessages } from "@stores/chats";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature, nip04 } from "nostr-tools"; import { nip04 } from "nostr-tools";
import { useCallback, useContext, useState } from "react"; import { useCallback, useContext, useState } from "react";
export function ChatMessageForm({ export function ChatMessageForm({
@ -12,8 +12,9 @@ export function ChatMessageForm({
userPubkey, userPubkey,
userPrivkey, userPrivkey,
}: { receiverPubkey: string; userPubkey: string; userPrivkey: string }) { }: { receiverPubkey: string; userPubkey: string; userPrivkey: string }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const addMessage = useChatMessages((state: any) => state.add); const addMessage = useChatMessages((state: any) => state.add);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const encryptMessage = useCallback(async () => { const encryptMessage = useCallback(async () => {
@ -23,19 +24,19 @@ export function ChatMessageForm({
const submit = async () => { const submit = async () => {
const message = await encryptMessage(); const message = await encryptMessage();
const event: any = { const signer = new NDKPrivateKeySigner(userPrivkey);
content: message, ndk.signer = signer;
created_at: dateToUnix(),
kind: 4,
pubkey: userPubkey,
tags: [["p", receiverPubkey]],
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, userPrivkey); // build event
event.content = message;
event.kind = 4;
event.created_at = dateToUnix();
event.pubkey = userPubkey;
event.tags = [["p", receiverPubkey]];
// publish message // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// add message to store // add message to store
addMessage(receiverPubkey, event); addMessage(receiverPubkey, event);

View File

@ -28,7 +28,7 @@ export function ChatMessageUser({
<> <>
<div className="relative h-11 w-11 shrink rounded-md"> <div className="relative h-11 w-11 shrink rounded-md">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />

View File

@ -34,7 +34,7 @@ export function ChatsListSelfItem({ data }: { data: any }) {
> >
<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?.image || DEFAULT_AVATAR}
alt={data.pubkey} alt={data.pubkey}
className="h-5 w-5 rounded bg-white object-cover" className="h-5 w-5 rounded bg-white object-cover"
/> />

View File

@ -11,7 +11,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="relative h-11 w-11 shrink rounded-md"> <div className="relative h-11 w-11 shrink rounded-md">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />
@ -19,10 +19,10 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3 className="leading-none text-lg font-semibold"> <h3 className="leading-none text-lg font-semibold">
{user?.display_name || user?.name} {user?.displayName || user?.name}
</h3> </h3>
<h5 className="leading-none text-zinc-400"> <h5 className="leading-none text-zinc-400">
{user?.nip05 || user?.username || shortenKey(pubkey)} {user?.nip05 || shortenKey(pubkey)}
</h5> </h5>
</div> </div>
<div> <div>

View File

@ -4,14 +4,13 @@ import { ChatMessageForm } from "@app/chat/components/messages/form";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { useChatMessages } from "@stores/chats"; import { useChatMessages } from "@stores/chats";
import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { usePageContext } from "@utils/hooks/usePageContext"; import { usePageContext } from "@utils/hooks/usePageContext";
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const pageContext = usePageContext(); const pageContext = usePageContext();
@ -25,23 +24,19 @@ export function Page() {
const add = useChatMessages((state: any) => state.add); const add = useChatMessages((state: any) => state.add);
useSWRSubscription(account !== pubkey ? ["chat", pubkey] : null, () => { useSWRSubscription(account !== pubkey ? ["chat", pubkey] : null, () => {
const unsubscribe = pool.subscribe( const sub = ndk.subscribe({
[ kinds: [4],
{ authors: [pubkey],
kinds: [4], "#p": [account.pubkey],
authors: [pubkey], since: dateToUnix(),
"#p": [account.pubkey], });
since: dateToUnix(),
}, sub.addListener("event", (event: any) => {
], add(account.pubkey, event);
READONLY_RELAYS, });
(event: any) => {
add(account.pubkey, event);
},
);
return () => { return () => {
unsubscribe(); sub.stop();
}; };
}); });

View File

@ -14,7 +14,7 @@ export function MentionUser(props: { children: any[] }) {
return ( return (
<span className="text-fuchsia-500"> <span className="text-fuchsia-500">
@{user?.name || user?.display_name || shortenKey(pubkey)} @{user?.name || user?.displayName || shortenKey(pubkey)}
</span> </span>
); );
} }

View File

@ -1,9 +1,9 @@
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 { NDKEvent } from "@nostr-dev-kit/ndk";
import { NDKSubscription } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { createReplyNote } from "@utils/storage"; import { createReplyNote } from "@utils/storage";
import { decode } from "light-bolt11-decoder"; import { decode } from "light-bolt11-decoder";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
@ -16,65 +16,57 @@ export function NoteMetadata({
id: string; id: string;
eventPubkey: string; eventPubkey: string;
}) { }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const [replies, setReplies] = useState(0); const [replies, setReplies] = useState(0);
const [reposts, setReposts] = useState(0); const [reposts, setReposts] = useState(0);
const [zaps, setZaps] = useState(0); const [zaps, setZaps] = useState(0);
useSWRSubscription(id ? ["note-metadata", id] : null, ([, key]) => { useSWRSubscription(id ? ["note-metadata", id] : null, () => {
const unsubscribe = pool.subscribe( const sub: NDKSubscription = ndk.subscribe(
[
{
"#e": [key],
kinds: [1, 6, 9735],
limit: 20,
},
],
READONLY_RELAYS,
(event: any) => {
switch (event.kind) {
case 1:
setReplies((replies) => replies + 1);
createReplyNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
key,
);
break;
case 6:
setReposts((reposts) => reposts + 1);
break;
case 9735: {
const bolt11 = event.tags.find((tag) => tag[0] === "bolt11")[1];
if (bolt11) {
const decoded = decode(bolt11);
const amount = decoded.sections.find(
(item) => item.name === "amount",
);
setZaps(amount.value / 1000);
}
break;
}
default:
break;
}
},
undefined,
undefined,
{ {
unsubscribeOnEose: true, "#e": [id],
kinds: [1, 6, 9735],
limit: 20,
}, },
{ closeOnEose: false },
); );
sub.addListener("event", (event: NDKEvent) => {
switch (event.kind) {
case 1:
setReplies((replies) => replies + 1);
createReplyNote(
event.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
id,
);
break;
case 6:
setReposts((reposts) => reposts + 1);
break;
case 9735: {
const bolt11 = event.tags.find((tag) => tag[0] === "bolt11")[1];
if (bolt11) {
const decoded = decode(bolt11);
const amount = decoded.sections.find(
(item) => item.name === "amount",
);
setZaps(amount.value / 1000);
}
break;
}
default:
break;
}
});
return () => { return () => {
unsubscribe(); sub.stop();
}; };
}); });

View File

@ -1,62 +0,0 @@
import { LikeIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react";
export function NoteLike({
id,
pubkey,
likes,
}: { id: string; pubkey: string; likes: number }) {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const [count, setCount] = useState(0);
const submitEvent = (e: any) => {
e.stopPropagation();
const event: any = {
content: "+",
kind: 7,
tags: [
["e", id],
["p", pubkey],
],
created_at: dateToUnix(),
pubkey: account.pubkey,
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// publish event to all relays
pool.publish(event, WRITEONLY_RELAYS);
// update state
setCount(count + 1);
};
useEffect(() => {
setCount(likes);
}, [likes]);
return (
<button
type="button"
onClick={(e) => submitEvent(e)}
className="group inline-flex items-center gap-1.5"
>
<LikeIcon
width={16}
height={16}
className="text-zinc-400 group-hover:text-rose-400"
/>
<span className="text-base leading-none text-zinc-400 group-hover:text-white">
{count}
</span>
</button>
);
}

View File

@ -1,16 +1,15 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { ReplyIcon } from "@shared/icons"; import { ReplyIcon } from "@shared/icons";
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { compactNumber } from "@utils/number"; import { compactNumber } from "@utils/number";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react"; import { Fragment, useContext, useEffect, useState } from "react";
export function NoteReply({ id, replies }: { id: string; replies: number }) { export function NoteReply({ id, replies }: { id: string; replies: number }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
@ -26,19 +25,19 @@ export function NoteReply({ id, replies }: { id: string; replies: number }) {
}; };
const submitEvent = () => { const submitEvent = () => {
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: value, ndk.signer = signer;
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [["e", id]],
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = value;
event.kind = 1;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [["e", id]];
// publish event // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// close modal // close modal
setIsOpen(false); setIsOpen(false);
@ -96,7 +95,7 @@ export function NoteReply({ id, replies }: { id: string; replies: number }) {
<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={account?.picture} src={account?.image}
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

@ -1,10 +1,9 @@
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { RepostIcon } from "@shared/icons"; import { RepostIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { compactNumber } from "@utils/number"; import { compactNumber } from "@utils/number";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
export function NoteRepost({ export function NoteRepost({
@ -12,7 +11,7 @@ export function NoteRepost({
pubkey, pubkey,
reposts, reposts,
}: { id: string; pubkey: string; reposts: number }) { }: { id: string; pubkey: string; reposts: number }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
@ -20,22 +19,22 @@ export function NoteRepost({
const submitEvent = (e: any) => { const submitEvent = (e: any) => {
e.stopPropagation(); e.stopPropagation();
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: "", ndk.signer = signer;
kind: 6,
tags: [
["e", id],
["p", pubkey],
],
created_at: dateToUnix(),
pubkey: account.pubkey,
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = "";
event.kind = 6;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [
["e", id],
["p", pubkey],
];
// publish event to all relays // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// update state // update state
setCount(count + 1); setCount(count + 1);

View File

@ -17,11 +17,13 @@ export function LinkPreview({ urls }: { urls: string[] }) {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<Image {data["og:image"] && (
src={data["og:image"]} <Image
alt={urls[0]} src={data["og:image"]}
className="w-full h-auto border-t-lg object-cover" alt={urls[0]}
/> className="w-full h-auto border-t-lg object-cover"
/>
)}
<div className="flex flex-col gap-2 px-3 py-3"> <div className="flex flex-col gap-2 px-3 py-3">
<h5 className="leading-none font-medium text-zinc-200"> <h5 className="leading-none font-medium text-zinc-200">
{data["og:title"]} {data["og:title"]}

View File

@ -1,32 +1,29 @@
import { Image } from "@shared/image"; import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { useProfile } from "@utils/hooks/useProfile";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
export function NoteReplyForm({ id }: { id: string }) { export function NoteReplyForm({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const submitEvent = () => { const submitEvent = () => {
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: value, ndk.signer = signer;
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [["e", id]],
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = value;
event.kind = 1;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [["e", id]];
// publish note // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// reset form // reset form
setValue(""); setValue("");

View File

@ -1,34 +1,35 @@
import { NoteReplyForm } from "@app/note/components/replies/form"; import { NoteReplyForm } from "@app/note/components/replies/form";
import { Reply } from "@app/note/components/replies/item"; import { Reply } from "@app/note/components/replies/item";
import { NostrEvent } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from "@stores/constants";
import { sortEvents } from "@utils/transform"; import { sortEvents } from "@utils/transform";
import { useContext } from "react"; import { useContext } from "react";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
export function RepliesList({ id }: { id: string }) { export function RepliesList({ id }: { id: string }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const { data, error } = useSWRSubscription( const { data, error } = useSWRSubscription(
id ? ["note-replies", id] : null, id ? ["note-replies", id] : null,
([, key], { next }) => { ([, key], { next }) => {
// subscribe to note // subscribe to note
const unsubscribe = pool.subscribe( const sub = ndk.subscribe(
[ {
{ "#e": [key],
"#e": [key], kinds: [1],
kinds: [1], limit: 20,
limit: 20, },
}, {
], closeOnEose: true,
READONLY_RELAYS,
(event: any) => {
next(null, (prev: any) => (prev ? [...prev, event] : [event]));
}, },
); );
sub.addListener("event", (event: NostrEvent) => {
next(null, (prev: any) => (prev ? [...prev, event] : [event]));
});
return () => { return () => {
unsubscribe(); sub.stop();
}; };
}, },
); );

View File

@ -3,8 +3,8 @@ import { Kind1063 } from "@app/note/components/kind1063";
import { NoteMetadata } from "@app/note/components/metadata"; import { NoteMetadata } from "@app/note/components/metadata";
import { NoteSkeleton } from "@app/note/components/skeleton"; import { NoteSkeleton } from "@app/note/components/skeleton";
import { NoteDefaultUser } from "@app/note/components/user/default"; import { NoteDefaultUser } from "@app/note/components/user/default";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from "@stores/constants";
import { noteParser } from "@utils/parser"; import { noteParser } from "@utils/parser";
import { memo, useContext } from "react"; import { memo, useContext } from "react";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
@ -23,31 +23,22 @@ export const RootNote = memo(function RootNote({
id, id,
fallback, fallback,
}: { id: string; fallback?: any }) { }: { id: string; fallback?: any }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const parseFallback = isJSON(fallback) ? JSON.parse(fallback) : null; const parseFallback = isJSON(fallback) ? JSON.parse(fallback) : null;
const { data, error } = useSWRSubscription( const { data, error } = useSWRSubscription(
parseFallback ? null : id, parseFallback ? null : id,
(key, { next }) => { (key, { next }) => {
const unsubscribe = pool.subscribe( const sub = ndk.subscribe({
[ ids: [key],
{ });
ids: [key],
}, sub.addListener("event", (event: NDKEvent) => {
], next(null, event);
READONLY_RELAYS, });
(event: any) => {
next(null, event);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
},
);
return () => { return () => {
unsubscribe(); sub.stop();
}; };
}, },
); );

View File

@ -19,7 +19,7 @@ export function NoteDefaultUser({
<Popover className="relative flex items-start gap-3"> <Popover className="relative flex items-start gap-3">
<Popover.Button className="h-11 w-11 shrink-0 overflow-hidden rounded-md bg-zinc-900"> <Popover.Button className="h-11 w-11 shrink-0 overflow-hidden rounded-md bg-zinc-900">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 object-cover" className="h-11 w-11 object-cover"
/> />
@ -50,14 +50,14 @@ export function NoteDefaultUser({
> >
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3"> <div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-14 w-14 shrink-0 rounded-lg object-cover" className="h-14 w-14 shrink-0 rounded-lg object-cover"
/> />
<div className="flex w-full flex-1 flex-col gap-2"> <div className="flex w-full flex-1 flex-col gap-2">
<div className="inline-flex w-2/3 flex-col gap-0.5"> <div className="inline-flex w-2/3 flex-col gap-0.5">
<h5 className="text-base font-semibold leading-none"> <h5 className="text-base font-semibold leading-none">
{user?.display_name || user?.name || ( {user?.displayName || user?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" /> <div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)} )}
</h5> </h5>

View File

@ -20,7 +20,7 @@ export function NoteQuoteUser({
<div className="group flex items-center gap-2"> <div className="group flex items-center gap-2">
<div className="relative h-6 w-6 shrink-0 rounded"> <div className="relative h-6 w-6 shrink-0 rounded">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-6 w-6 rounded object-cover" className="h-6 w-6 rounded object-cover"
/> />

View File

@ -20,7 +20,7 @@ export function NoteReplyUser({
<div className="group flex items-start gap-2.5"> <div className="group flex items-start gap-2.5">
<div className="relative h-11 w-11 shrink-0 rounded-md"> <div className="relative h-11 w-11 shrink-0 rounded-md">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />

View File

@ -19,7 +19,7 @@ export function NoteRepostUser({
<Popover className="relative flex items-start gap-3"> <Popover className="relative flex items-start gap-3">
<Popover.Button className="h-11 w-11 shrink-0 overflow-hidden rounded-md bg-zinc-900"> <Popover.Button className="h-11 w-11 shrink-0 overflow-hidden rounded-md bg-zinc-900">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />
@ -54,14 +54,14 @@ export function NoteRepostUser({
> >
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3"> <div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-14 w-14 shrink-0 rounded-lg object-cover" className="h-14 w-14 shrink-0 rounded-lg object-cover"
/> />
<div className="flex w-full flex-1 flex-col gap-2"> <div className="flex w-full flex-1 flex-col gap-2">
<div className="inline-flex w-2/3 flex-col gap-0.5"> <div className="inline-flex w-2/3 flex-col gap-0.5">
<h5 className="text-base font-semibold leading-none"> <h5 className="text-base font-semibold leading-none">
{user?.display_name || user?.name || ( {user?.displayName || user?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" /> <div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)} )}
</h5> </h5>

View File

@ -2,41 +2,19 @@ import { Kind1 } from "@app/note/components/kind1";
import { NoteMetadata } from "@app/note/components/metadata"; import { NoteMetadata } from "@app/note/components/metadata";
import { RepliesList } from "@app/note/components/replies/list"; import { RepliesList } from "@app/note/components/replies/list";
import { NoteDefaultUser } from "@app/note/components/user/default"; import { NoteDefaultUser } from "@app/note/components/user/default";
import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from "@stores/constants";
import { usePageContext } from "@utils/hooks/usePageContext"; import { usePageContext } from "@utils/hooks/usePageContext";
import { noteParser } from "@utils/parser"; import { noteParser } from "@utils/parser";
import { useContext } from "react"; import { getNoteByID } from "@utils/storage";
import useSWRSubscription from "swr/subscription"; import useSWR from "swr";
const fetcher = ([, id]) => getNoteByID(id);
export function Page() { export function Page() {
const pool: any = useContext(RelayContext);
const pageContext = usePageContext(); const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search; const searchParams: any = pageContext.urlParsed.search;
const noteID = searchParams.id; const noteID = searchParams.id;
const { data, error } = useSWRSubscription( const { data, error } = useSWR(["note", noteID], fetcher);
noteID ? ["note", noteID] : null,
([, key], { next }) => {
// subscribe to note
const unsubscribe = pool.subscribe(
[
{
ids: [key],
},
],
READONLY_RELAYS,
(event: any) => {
next(null, event);
},
);
return () => {
unsubscribe();
};
},
);
const content = !error && data ? noteParser(data) : null; const content = !error && data ? noteParser(data) : null;

View File

@ -1,7 +1,7 @@
import { NDKFilter } from "@nostr-dev-kit/ndk";
import { LumeIcon } from "@shared/icons"; import { LumeIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from "@utils/date"; import { dateToUnix, getHourAgo } from "@utils/date";
import { import {
addToBlacklist, addToBlacklist,
@ -9,9 +9,7 @@ import {
createChat, createChat,
createNote, createNote,
} from "@utils/storage"; } from "@utils/storage";
import { getParentID } from "@utils/transform"; import { useContext, useEffect, useRef } from "react";
import { useCallback, useContext, useRef } from "react";
import useSWRSubscription from "swr/subscription";
import { navigate } from "vite-plugin-ssr/client/router"; import { navigate } from "vite-plugin-ssr/client/router";
let totalNotes: number; let totalNotes: number;
@ -21,127 +19,69 @@ if (typeof window !== "undefined") {
} }
export function Page() { export function Page() {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const now = useRef(new Date());
const [account, lastLogin] = useActiveAccount((state: any) => [ const [account, lastLogin] = useActiveAccount((state: any) => [
state.account, state.account,
state.lastLogin, state.lastLogin,
]); ]);
const now = useRef(new Date()); async function fetchNotes() {
const eose = useRef(0); try {
const follows = JSON.parse(account.follows);
let queryNoteSince: number;
const getQuery = useCallback(() => { if (totalNotes === 0) {
const query = [];
const follows = JSON.parse(account.follows);
let queryNoteSince: number;
if (totalNotes === 0) {
queryNoteSince = dateToUnix(getHourAgo(48, now.current));
} else {
if (lastLogin > 0) {
queryNoteSince = lastLogin;
} else {
queryNoteSince = dateToUnix(getHourAgo(48, now.current)); queryNoteSince = dateToUnix(getHourAgo(48, now.current));
} else {
if (lastLogin > 0) {
queryNoteSince = lastLogin;
} else {
queryNoteSince = dateToUnix(getHourAgo(48, now.current));
}
} }
const filter: NDKFilter = {
kinds: [1, 6],
authors: follows,
since: queryNoteSince,
};
const events = await ndk.fetchEvents(filter);
events.forEach((event) => {
createNote(
event.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
);
});
return true;
} catch (e) {
console.log("error: ", e);
} }
}
// kind 1 (notes) query async function fetchChannelBlacklist() {
query.push({ try {
kinds: [1, 6], const filter: NDKFilter = {
authors: follows, authors: [account.pubkey],
since: queryNoteSince, kinds: [43, 44],
}); since: lastLogin,
};
// kind 4 (chats) query const events = await ndk.fetchEvents(filter);
query.push({ events.forEach((event) => {
kinds: [4],
"#p": [account.pubkey],
since: lastLogin,
});
// kind 4 (chats) query
query.push({
kinds: [4],
authors: [account.pubkey],
since: lastLogin,
});
// kind 43, 43 (mute user, hide message) query
query.push({
authors: [account.pubkey],
kinds: [43, 44],
since: lastLogin,
});
return query;
}, [account]);
useSWRSubscription(account ? "prefetch" : null, () => {
let timeout: string | number | NodeJS.Timeout;
const query = getQuery();
const unsubscribe = pool.subscribe(
query,
READONLY_RELAYS,
(event: any) => {
switch (event.kind) { switch (event.kind) {
// short text note
case 1: {
// insert event to local database
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
);
break;
}
// chat
case 4: {
if (event.pubkey === account.pubkey) {
const receiver = event.tags.find((t) => t[0] === "p")[1];
createChat(
event.id,
receiver,
event.pubkey,
event.content,
event.tags,
event.created_at,
);
} else {
createChat(
event.id,
account.pubkey,
event.pubkey,
event.content,
event.tags,
event.created_at,
);
}
break;
}
// repost
case 6:
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
);
break;
// hide message (channel only)
case 43: case 43:
if (event.tags[0][0] === "e") { if (event.tags[0][0] === "e") {
addToBlacklist(account.id, event.tags[0][1], 43, 1); addToBlacklist(account.id, event.tags[0][1], 43, 1);
} }
break; break;
// mute user (channel only)
case 44: case 44:
if (event.tags[0][0] === "p") { if (event.tags[0][0] === "p") {
addToBlacklist(account.id, event.tags[0][1], 44, 1); addToBlacklist(account.id, event.tags[0][1], 44, 1);
@ -150,37 +90,85 @@ export function Page() {
default: default:
break; break;
} }
}, });
undefined,
() => {
eose.current += 1;
if (eose.current === READONLY_RELAYS.length) {
timeout = setTimeout(
() => navigate("/app/space", { overwriteLastHistoryEntry: true }),
2000,
);
}
},
{
unsubscribeOnEose: true,
},
);
return () => { return true;
unsubscribe(); } catch (e) {
clearTimeout(timeout); console.log("error: ", e);
}; }
}); }
async function fetchReceiveMessages() {
try {
const filter: NDKFilter = {
kinds: [4],
"#p": [account.pubkey],
since: lastLogin,
};
const events = await ndk.fetchEvents(filter);
events.forEach((event) => {
createChat(
event.id,
account.pubkey,
event.pubkey,
event.content,
event.tags,
event.created_at,
);
});
return true;
} catch (e) {
console.log("error: ", e);
}
}
async function fetchSendMessages() {
try {
const filter: NDKFilter = {
kinds: [4],
authors: [account.pubkey],
since: lastLogin,
};
const events = await ndk.fetchEvents(filter);
events.forEach((event) => {
const receiver = event.tags.find((t) => t[0] === "p")[1];
createChat(
event.id,
receiver,
account.pubkey,
event.content,
event.tags,
event.created_at,
);
});
return true;
} catch (e) {
console.log("error: ", e);
}
}
useEffect(() => {
async function prefetch() {
const notes = await fetchNotes();
if (notes) {
navigate("/app/space", { overwriteLastHistoryEntry: true });
}
}
prefetch();
}, []);
return ( return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white"> <div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
<div className="relative h-full overflow-hidden"> <div className="relative h-full overflow-hidden">
{/* dragging area */}
<div <div
data-tauri-drag-region data-tauri-drag-region
className="absolute left-0 top-0 z-20 h-16 w-full bg-transparent" className="absolute left-0 top-0 z-20 h-16 w-full bg-transparent"
/> />
{/* end dragging area */}
<div className="relative flex h-full flex-col items-center justify-center"> <div className="relative flex h-full flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<LumeIcon className="h-16 w-16 text-black dark:text-white" /> <LumeIcon className="h-16 w-16 text-black dark:text-white" />

View File

@ -1,24 +1,23 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { CancelIcon, ImageIcon } from "@shared/icons"; import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CancelIcon } from "@shared/icons";
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { open } from "@tauri-apps/api/dialog"; import { open } from "@tauri-apps/api/dialog";
import { Body, fetch } from "@tauri-apps/api/http"; import { Body, fetch } from "@tauri-apps/api/http";
import { createBlobFromFile } from "@utils/createBlobFromFile"; import { createBlobFromFile } from "@utils/createBlobFromFile";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useRef, useState } from "react"; import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
export function AddImageBlock({ parentState }: { parentState: any }) { export function AddImageBlock({ parentState }: { parentState: any }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const [account, addBlock] = useActiveAccount((state: any) => [ const [account, addBlock] = useActiveAccount((state: any) => [
state.account, state.account,
state.addBlock, state.addBlock,
]); ]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(true);
const [image, setImage] = useState(""); const [image, setImage] = useState("");
@ -91,19 +90,19 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
setLoading(true); setLoading(true);
const event: any = { const signer = new NDKPrivateKeySigner(account.privkey);
content: data.title, ndk.signer = signer;
created_at: dateToUnix(),
kind: 1063,
pubkey: account.pubkey,
tags: tags.current,
};
event.id = getEventHash(event); const event = new NDKEvent(ndk);
event.sig = getSignature(event, account.privkey); // build event
event.content = data.title;
event.kind = 1063;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = tags.current;
// publish channel // publish event
pool.publish(event, WRITEONLY_RELAYS); event.publish();
// insert to database // insert to database
addBlock(0, data.title, data.content); addBlock(0, data.title, data.content);

15
src/libs/ndk.tsx Normal file
View File

@ -0,0 +1,15 @@
import NDK, { NDKConstructorParams } from "@nostr-dev-kit/ndk";
import { FULL_RELAYS } from "@stores/constants";
export async function initNDK(
relays?: string[],
cache?: boolean,
): Promise<NDK> {
const opts: NDKConstructorParams = {};
opts.explicitRelayUrls = relays || FULL_RELAYS;
const ndk = new NDK(opts);
await ndk.connect();
return ndk;
}

View File

@ -3,17 +3,15 @@ import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts"; import { useActiveAccount } from "@stores/accounts";
import { useChannels } from "@stores/channels"; import { useChannels } from "@stores/channels";
import { useChatMessages, useChats } from "@stores/chats"; import { useChatMessages, useChats } from "@stores/chats";
import { DEFAULT_AVATAR, READONLY_RELAYS } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { usePageContext } from "@utils/hooks/usePageContext"; import { usePageContext } from "@utils/hooks/usePageContext";
import { useProfile } from "@utils/hooks/useProfile"; import { useProfile } from "@utils/hooks/useProfile";
import { sendNativeNotification } from "@utils/notification"; import { sendNativeNotification } from "@utils/notification";
import { createNote } from "@utils/storage";
import { useContext } from "react"; import { useContext } from "react";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
export function ActiveAccount({ data }: { data: any }) { export function ActiveAccount({ data }: { data: any }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const account = useActiveAccount((state: any) => state.account);
const pageContext = usePageContext(); const pageContext = usePageContext();
@ -35,60 +33,38 @@ export function ActiveAccount({ data }: { data: any }) {
() => { () => {
const follows = JSON.parse(account.follows); const follows = JSON.parse(account.follows);
// subscribe to channel // subscribe to channel
const unsubscribe = pool.subscribe( const sub = ndk.subscribe({
[ "#p": [data.pubkey],
{ since: lastLogin,
kinds: [1, 6], });
authors: follows,
since: dateToUnix(), sub.addListener("event", (event) => {
}, switch (event.kind) {
{ case 4:
"#p": [data.pubkey], if (!isChatPage) {
since: lastLogin, // save
}, saveChat(data.pubkey, event);
], // update state
READONLY_RELAYS, notifyChat(event.pubkey);
(event) => { // send native notifiation
switch (event.kind) { sendNativeNotification("You've received new message");
case 1:
case 6: {
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
);
break;
} }
case 4: break;
if (!isChatPage) { case 42:
// save if (!isChannelPage) {
saveChat(data.pubkey, event); // update state
// update state notifyChannel(event);
notifyChat(event.pubkey); // send native notifiation
// send native notifiation sendNativeNotification(event.content);
sendNativeNotification("You've received new message"); }
} break;
break; default:
case 42: break;
if (!isChannelPage) { }
// update state });
notifyChannel(event);
// send native notifiation
sendNativeNotification(event.content);
}
break;
default:
break;
}
},
);
return () => { return () => {
unsubscribe(); sub.stop();
}; };
}, },
); );
@ -96,7 +72,7 @@ export function ActiveAccount({ data }: { data: any }) {
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={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={data.npub} alt={data.npub}
className="h-11 w-11 rounded-md object-cover" className="h-11 w-11 rounded-md object-cover"
/> />

View File

@ -9,7 +9,7 @@ export function InactiveAccount({ data }: { data: any }) {
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={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={data.npub} alt={data.npub}
className="h-11 w-11 rounded-lg object-cover" className="h-11 w-11 rounded-lg object-cover"
/> />

View File

@ -1,9 +1,8 @@
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { ImageUploader } from "@shared/composer/imageUploader"; import { ImageUploader } from "@shared/composer/imageUploader";
import { TrashIcon } from "@shared/icons"; import { TrashIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date"; import { dateToUnix } from "@utils/date";
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";
@ -59,12 +58,13 @@ const ImagePreview = ({
}; };
export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) { export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const editor = useMemo( const editor = useMemo(
() => withReact(withImages(withHistory(createEditor()))), () => withReact(withImages(withHistory(createEditor()))),
[], [],
); );
const [content, setContent] = useState<Node[]>([ const [content, setContent] = useState<Node[]>([
{ {
children: [ children: [
@ -82,20 +82,19 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const submit = () => { const submit = () => {
// serialize content // serialize content
const serializedContent = serialize(content); const serializedContent = serialize(content);
console.log(serializedContent);
const event: any = { const signer = new NDKPrivateKeySigner(privkey);
content: serializedContent, ndk.signer = signer;
created_at: dateToUnix(),
kind: 1,
pubkey: pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = getSignature(event, privkey);
// publish note const event = new NDKEvent(ndk);
pool.publish(event, WRITEONLY_RELAYS); event.kind = 1;
event.content = serializedContent;
event.created_at = dateToUnix();
event.pubkey = pubkey;
event.tags = [];
// publish event
event.publish();
}; };
const renderElement = useCallback((props: any) => { const renderElement = useCallback((props: any) => {

View File

@ -9,7 +9,7 @@ export function User({ pubkey }: { pubkey: string }) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-8 w-8 shrink-0 overflow-hidden rounded bg-zinc-900"> <div className="h-8 w-8 shrink-0 overflow-hidden rounded bg-zinc-900">
<Image <Image
src={user?.picture || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className="h-8 w-8 object-cover" className="h-8 w-8 object-cover"
loading="auto" loading="auto"

View File

@ -1,16 +1,10 @@
import { FULL_RELAYS } from "@stores/constants"; import { initNDK } from "@libs/ndk";
import { RelayPool } from "nostr-relaypool"; import NDK from "@nostr-dev-kit/ndk";
import { createContext } from "react"; import { createContext } from "react";
export const RelayContext = createContext({}); export const RelayContext = createContext<NDK>(null);
const ndk = await initNDK();
const pool = new RelayPool(FULL_RELAYS, {
useEventCache: false,
subscriptionCache: true,
logErrorsAndNotices: false,
logSubscriptions: false,
});
export function RelayProvider({ children }: { children: React.ReactNode }) { export function RelayProvider({ children }: { children: React.ReactNode }) {
return <RelayContext.Provider value={pool}>{children}</RelayContext.Provider>; return <RelayContext.Provider value={ndk}>{children}</RelayContext.Provider>;
} }

View File

@ -28,4 +28,7 @@ export const FULL_RELAYS = [
"wss://welcome.nostr.wine", "wss://welcome.nostr.wine",
"wss://relay.nostr.band", "wss://relay.nostr.band",
"wss://relay.damus.io", "wss://relay.damus.io",
"wss://relay.snort.social",
"wss://relayable.org",
"wss://nostr.mutinywallet.com",
]; ];

View File

@ -1,56 +1,31 @@
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { createNote, getNoteByID } from "@utils/storage"; import { createNote, getNoteByID } from "@utils/storage";
import { getParentID } from "@utils/transform";
import { useContext } from "react"; import { useContext } from "react";
import useSWR from "swr"; import useSWR from "swr";
import useSWRSubscription from "swr/subscription";
const fetcher = ([, id]) => getNoteByID(id); const fetcher = async ([, ndk, id]) => {
const result = await getNoteByID(id);
if (result) {
return result;
} else {
const event = await ndk.fetchEvent(id);
await createNote(
event.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
);
event["event_id"] = event.id;
return event;
}
};
export function useEvent(id: string) { export function useEvent(id: string) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account); const { data } = useSWR(["note", ndk, id], fetcher);
const { data: cache } = useSWR(["event", id], fetcher); return data;
const { data: newest } = useSWRSubscription(
!cache ? id : null,
(key, { next }) => {
const unsubscribe = pool.subscribe(
[
{
ids: [key],
},
],
READONLY_RELAYS,
(event: any) => {
const parentID = getParentID(event.tags, event.id);
// insert event to local database
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
);
// update state
next(null, event);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
},
);
return () => {
unsubscribe();
};
},
);
return cache ? cache : newest;
} }

View File

@ -1,12 +1,11 @@
import NDK from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { METADATA_RELAY } from "@stores/constants";
import { createPleb, getPleb } from "@utils/storage"; import { createPleb, getPleb } from "@utils/storage";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { useContext } from "react"; import { useContext } from "react";
import useSWR from "swr"; import useSWR from "swr";
import useSWRSubscription from "swr/subscription";
const fetcher = async (key: string) => { const fetcher = async ([, ndk, key]) => {
let npub: string; let npub: string;
if (key.substring(0, 4) === "npub") { if (key.substring(0, 4) === "npub") {
@ -18,59 +17,27 @@ const fetcher = async (key: string) => {
const current = Math.floor(Date.now() / 1000); const current = Math.floor(Date.now() / 1000);
const result = await getPleb(npub); const result = await getPleb(npub);
if (result && result.created_at + 86400 < current) { if (result && result.created_at + 86400 > current) {
return result; return result;
} else { } else {
return null; const user = ndk.getUser({ npub });
await user.fetchProfile();
await createPleb(key, user.profile);
return user.profile;
} }
}; };
export function useProfile(key: string) { export function useProfile(key: string) {
const pool: any = useContext(RelayContext); const ndk = useContext(RelayContext);
const { data, error, isLoading } = useSWR(["profile", ndk, key], fetcher, {
const {
data: cache,
error,
isLoading,
} = useSWR(key, fetcher, {
revalidateIfStale: false, revalidateIfStale: false,
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: true, revalidateOnReconnect: true,
}); });
const { data: newest } = useSWRSubscription(
cache ? null : key,
(_, { next }) => {
const unsubscribe = pool.subscribe(
[
{
authors: [key],
kinds: [0],
},
],
METADATA_RELAY,
(event: { content: string }) => {
const content = JSON.parse(event.content);
// update state
next(null, content);
// save to database
createPleb(key, event);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
},
);
return () => {
unsubscribe();
};
},
);
return { return {
user: newest ? newest : cache, user: data,
isLoading, isLoading,
isError: error, isError: error,
}; };

View File

@ -1,3 +1,4 @@
import { NDKTag, NDKUserProfile } from "@nostr-dev-kit/ndk";
import { getParentID } from "@utils/transform"; import { getParentID } from "@utils/transform";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import Database from "tauri-plugin-sql-api"; import Database from "tauri-plugin-sql-api";
@ -76,10 +77,9 @@ export async function getPleb(npub: string) {
} }
// create pleb // create pleb
export async function createPleb(key: string, json: any) { export async function createPleb(key: string, data: NDKUserProfile) {
const db = await connect(); const db = await connect();
const data = JSON.parse(json.content); const now = Math.floor(Date.now() / 1000);
let npub: string; let npub: string;
if (key.substring(0, 4) === "npub") { if (key.substring(0, 4) === "npub") {
@ -89,21 +89,20 @@ export async function createPleb(key: string, json: any) {
} }
return await db.execute( return await db.execute(
"INSERT OR REPLACE INTO plebs (npub, display_name, name, username, about, bio, website, picture, banner, nip05, lud06, lud16, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", "INSERT OR REPLACE INTO plebs (npub, name, displayName, image, banner, bio, nip05, lud06, lud16, about, zapService, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
[ [
npub, npub,
data.display_name || data.displayName,
data.name, data.name,
data.username, data.displayName,
data.about, data.image,
data.bio,
data.website,
data.picture || data.image,
data.banner, data.banner,
data.bio,
data.nip05, data.nip05,
data.lud06, data.lud06,
data.lud16, data.lud16,
data.created_at, data.about,
data.zapService,
now,
], ],
); );
} }
@ -216,39 +215,41 @@ export async function getLatestNotes(time: number) {
// create note // create note
export async function createNote( export async function createNote(
event_id: string, event_id: string,
account_id: number,
pubkey: string, pubkey: string,
kind: number, kind: number,
tags: string[], tags: any,
content: string, content: string,
created_at: number, created_at: number,
) { ) {
const db = await connect(); const db = await connect();
const account = await getActiveAccount();
const parentID = getParentID(tags, event_id); const parentID = getParentID(tags, event_id);
return await db.execute( return await db.execute(
"INSERT INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);", "INSERT OR IGNORE INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
[event_id, account_id, pubkey, kind, tags, content, created_at, parentID], [event_id, account.id, pubkey, kind, tags, content, created_at, parentID],
); );
} }
// create reply note // create reply note
export async function createReplyNote( export async function createReplyNote(
event_id: string, event_id: string,
account_id: number,
pubkey: string, pubkey: string,
kind: number, kind: number,
tags: string[], tags: any,
content: string, content: string,
created_at: number, created_at: number,
parent_comment_id: string, parent_comment_id: string,
) { ) {
const db = await connect(); const db = await connect();
const account = await getActiveAccount();
const parentID = getParentID(tags, event_id); const parentID = getParentID(tags, event_id);
return await db.execute( return await db.execute(
"INSERT INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id, parent_comment_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", "INSERT OR IGNORE INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id, parent_comment_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);",
[ [
event_id, event_id,
account_id, account.id,
pubkey, pubkey,
kind, kind,
tags, tags,
@ -298,7 +299,7 @@ export async function updateChannelMetadata(event_id: string, value: string) {
return await db.execute( return await db.execute(
"UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;", "UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;",
[data.name, data.picture, data.about, event_id], [data.name, data.image, data.about, event_id],
); );
} }

View File

@ -1,9 +1,20 @@
import destr from "destr"; import destr from "destr";
import { nip19 } from "nostr-tools";
export function truncateContent(str, n) { export function truncateContent(str, n) {
return str.length > n ? `${str.slice(0, n - 1)}&hellip;` : str; return str.length > n ? `${str.slice(0, n - 1)}&hellip;` : str;
} }
export function setToArray(tags: any) {
const newArray = [];
tags.forEach((item) => {
const hexpubkey = nip19.decode(item.npub).data;
newArray.push(hexpubkey);
});
return newArray;
}
// convert NIP-02 to array of pubkey // convert NIP-02 to array of pubkey
export function nip02ToArray(tags: any) { export function nip02ToArray(tags: any) {
const arr = []; const arr = [];

View File

@ -3,6 +3,7 @@
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@app/*": ["src/app/*"], "@app/*": ["src/app/*"],
"@libs/*": ["src/libs/*"],
"@shared/*": ["src/shared/*"], "@shared/*": ["src/shared/*"],
"@stores/*": ["src/stores/*"], "@stores/*": ["src/stores/*"],
"@utils/*": ["src/utils/*"] "@utils/*": ["src/utils/*"]