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": {
"@floating-ui/react": "^0.23.1",
"@headlessui/react": "^1.7.15",
"@nostr-dev-kit/ndk": "^0.4.4",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.3.0",
"@vidstack/react": "^0.4.5",
@ -29,7 +30,7 @@
"react-hook-form": "^7.44.3",
"react-markdown": "^8.0.7",
"react-resizable-panels": "^0.0.48",
"react-virtuoso": "^4.3.8",
"react-virtuoso": "^4.3.9",
"remark-gfm": "^3.0.1",
"slate": "^0.94.1",
"slate-history": "^0.93.0",
@ -44,7 +45,7 @@
"@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "^1.3.1",
"@types/node": "^18.16.16",
"@types/react": "^18.2.8",
"@types/react": "^18.2.9",
"@types/react-dom": "^18.2.4",
"@types/youtube-player": "^5.5.7",
"@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 (
id INTEGER NOT NULL PRIMARY KEY,
npub TEXT NOT NULL UNIQUE,
display_name TEXT,
name TEXT,
username TEXT,
about TEXT,
bio TEXT,
website TEXT,
picture TEXT,
displayName TEXT,
image TEXT,
banner TEXT,
bio TEXT,
nip05 TEXT,
lud06 TEXT,
lud16 TEXT,
about TEXT,
zapService TEXT,
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="relative h-11 w-11 shrink rounded-md">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-md object-cover"
decoding="async"
@ -18,7 +18,7 @@ export function User({ pubkey }: { pubkey: string }) {
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-white">
{user?.display_name || user?.name}
{user?.displayName || user?.name}
</span>
<span className="text-base leading-tight text-zinc-400">
{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 { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants";
import { getEventHash, getSignature } from "nostr-tools";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { navigate } from "vite-plugin-ssr/client/router";
export function Page() {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const [image, setImage] = useState(DEFAULT_AVATAR);
@ -25,28 +25,30 @@ export function Page() {
const onSubmit = (data: any) => {
setLoading(true);
const event: any = {
content: JSON.stringify(data),
created_at: Math.floor(Date.now() / 1000),
kind: 0,
pubkey: account.pubkey,
tags: [],
};
try {
const signer = new NDKPrivateKeySigner(account.privkey);
ndk.signer = signer;
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event = new NDKEvent(ndk);
// build event
event.content = JSON.stringify(data);
event.kind = 0;
event.pubkey = account.pubkey;
event.tags = [];
// publish event
event.publish();
// publish
pool.publish(event, WRITEONLY_RELAYS);
// redirect to step 3
setTimeout(
() =>
navigate("/app/auth/create/step-3", {
overwriteLastHistoryEntry: true,
}),
2000,
);
// redirect to step 3
setTimeout(
() =>
navigate("/app/auth/create/step-3", {
overwriteLastHistoryEntry: true,
}),
2000,
);
} catch {
console.log("error");
}
};
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">
<input
type={"text"}
{...register("display_name", {
{...register("displayName", {
required: true,
minLength: 4,
})}
@ -105,11 +107,11 @@ export function Page() {
</div>
<div className="flex flex-col gap-1">
<label className="text-base font-semibold uppercase tracking-wider text-zinc-400">
About
Bio
</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">
<textarea
{...register("about")}
{...register("bio")}
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"
/>
@ -119,7 +121,7 @@ export function Page() {
<button
type="submit"
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 ? (
<svg

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ export function Member({ pubkey }: { pubkey: string }) {
) : (
<Image
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"}
/>
)}

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ export function ChannelMessageUser({
<>
<div className="relative h-11 w-11 shrink-0 rounded-md">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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="relative shrink-0 rounded-md h-11 w-11">
<Image
src={metadata?.picture || DEFAULT_AVATAR}
src={metadata?.image || DEFAULT_AVATAR}
alt={id}
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="relative h-9 w-9 shrink rounded-md">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={data.content}
className="h-9 w-9 rounded-md object-cover"
/>
</div>
<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">
{user?.display_name || user?.name || "Pleb"}
{user?.displayName || user?.name || "Pleb"}
</span>
<span className="text-base leading-none text-zinc-400">
{shortenKey(data.content)}

View File

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

View File

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

View File

@ -34,7 +34,7 @@ export function ChatsListItem({ data }: { data: any }) {
>
<div className="relative h-5 w-5 shrink-0 rounded">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={data.sender_pubkey}
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="inline-flex items-baseline gap-1">
<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>
</div>
<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 { MediaUploader } from "@shared/mediaUploader";
import { RelayContext } from "@shared/relayProvider";
import { useChatMessages } from "@stores/chats";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature, nip04 } from "nostr-tools";
import { nip04 } from "nostr-tools";
import { useCallback, useContext, useState } from "react";
export function ChatMessageForm({
@ -12,8 +12,9 @@ export function ChatMessageForm({
userPubkey,
userPrivkey,
}: { receiverPubkey: string; userPubkey: string; userPrivkey: string }) {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const addMessage = useChatMessages((state: any) => state.add);
const [value, setValue] = useState("");
const encryptMessage = useCallback(async () => {
@ -23,19 +24,19 @@ export function ChatMessageForm({
const submit = async () => {
const message = await encryptMessage();
const event: any = {
content: message,
created_at: dateToUnix(),
kind: 4,
pubkey: userPubkey,
tags: [["p", receiverPubkey]],
};
const signer = new NDKPrivateKeySigner(userPrivkey);
ndk.signer = signer;
event.id = getEventHash(event);
event.sig = getSignature(event, userPrivkey);
const event = new NDKEvent(ndk);
// build event
event.content = message;
event.kind = 4;
event.created_at = dateToUnix();
event.pubkey = userPubkey;
event.tags = [["p", receiverPubkey]];
// publish message
pool.publish(event, WRITEONLY_RELAYS);
// publish event
event.publish();
// add message to store
addMessage(receiverPubkey, event);

View File

@ -28,7 +28,7 @@ export function ChatMessageUser({
<>
<div className="relative h-11 w-11 shrink rounded-md">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={data.pubkey}
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="relative h-11 w-11 shrink rounded-md">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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-1">
<h3 className="leading-none text-lg font-semibold">
{user?.display_name || user?.name}
{user?.displayName || user?.name}
</h3>
<h5 className="leading-none text-zinc-400">
{user?.nip05 || user?.username || shortenKey(pubkey)}
{user?.nip05 || shortenKey(pubkey)}
</h5>
</div>
<div>

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { NoteReply } from "@app/note/components/metadata/reply";
import { NoteRepost } from "@app/note/components/metadata/repost";
import { NoteZap } from "@app/note/components/metadata/zap";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { NDKSubscription } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { createReplyNote } from "@utils/storage";
import { decode } from "light-bolt11-decoder";
import { useContext, useState } from "react";
@ -16,65 +16,57 @@ export function NoteMetadata({
id: string;
eventPubkey: string;
}) {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const ndk = useContext(RelayContext);
const [replies, setReplies] = useState(0);
const [reposts, setReposts] = useState(0);
const [zaps, setZaps] = useState(0);
useSWRSubscription(id ? ["note-metadata", id] : null, ([, key]) => {
const unsubscribe = pool.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,
useSWRSubscription(id ? ["note-metadata", id] : null, () => {
const sub: NDKSubscription = ndk.subscribe(
{
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 () => {
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 { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { ReplyIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { compactNumber } from "@utils/number";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react";
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 [count, setCount] = useState(0);
@ -26,19 +25,19 @@ export function NoteReply({ id, replies }: { id: string; replies: number }) {
};
const submitEvent = () => {
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [["e", id]],
};
const signer = new NDKPrivateKeySigner(account.privkey);
ndk.signer = signer;
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event = new NDKEvent(ndk);
// build event
event.content = value;
event.kind = 1;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [["e", id]];
// publish event
pool.publish(event, WRITEONLY_RELAYS);
event.publish();
// close modal
setIsOpen(false);
@ -96,7 +95,7 @@ export function NoteReply({ id, replies }: { id: string; replies: number }) {
<div>
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
<Image
src={account?.picture}
src={account?.image}
alt="user's avatar"
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 { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { compactNumber } from "@utils/number";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react";
export function NoteRepost({
@ -12,7 +11,7 @@ export function NoteRepost({
pubkey,
reposts,
}: { id: string; pubkey: string; reposts: number }) {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const [count, setCount] = useState(0);
@ -20,22 +19,22 @@ export function NoteRepost({
const submitEvent = (e: any) => {
e.stopPropagation();
const event: any = {
content: "",
kind: 6,
tags: [
["e", id],
["p", pubkey],
],
created_at: dateToUnix(),
pubkey: account.pubkey,
};
const signer = new NDKPrivateKeySigner(account.privkey);
ndk.signer = signer;
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event = new NDKEvent(ndk);
// 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
pool.publish(event, WRITEONLY_RELAYS);
// publish event
event.publish();
// update state
setCount(count + 1);

View File

@ -17,11 +17,13 @@ export function LinkPreview({ urls }: { urls: string[] }) {
target="_blank"
rel="noreferrer"
>
<Image
src={data["og:image"]}
alt={urls[0]}
className="w-full h-auto border-t-lg object-cover"
/>
{data["og:image"] && (
<Image
src={data["og:image"]}
alt={urls[0]}
className="w-full h-auto border-t-lg object-cover"
/>
)}
<div className="flex flex-col gap-2 px-3 py-3">
<h5 className="leading-none font-medium text-zinc-200">
{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 { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useProfile } from "@utils/hooks/useProfile";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useState } from "react";
export function NoteReplyForm({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const [value, setValue] = useState("");
const submitEvent = () => {
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [["e", id]],
};
const signer = new NDKPrivateKeySigner(account.privkey);
ndk.signer = signer;
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event = new NDKEvent(ndk);
// build event
event.content = value;
event.kind = 1;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [["e", id]];
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// publish event
event.publish();
// reset form
setValue("");

View File

@ -1,34 +1,35 @@
import { NoteReplyForm } from "@app/note/components/replies/form";
import { Reply } from "@app/note/components/replies/item";
import { NostrEvent } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from "@stores/constants";
import { sortEvents } from "@utils/transform";
import { useContext } from "react";
import useSWRSubscription from "swr/subscription";
export function RepliesList({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const { data, error } = useSWRSubscription(
id ? ["note-replies", id] : null,
([, key], { next }) => {
// subscribe to note
const unsubscribe = pool.subscribe(
[
{
"#e": [key],
kinds: [1],
limit: 20,
},
],
READONLY_RELAYS,
(event: any) => {
next(null, (prev: any) => (prev ? [...prev, event] : [event]));
const sub = ndk.subscribe(
{
"#e": [key],
kinds: [1],
limit: 20,
},
{
closeOnEose: true,
},
);
sub.addListener("event", (event: NostrEvent) => {
next(null, (prev: any) => (prev ? [...prev, event] : [event]));
});
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 { NoteSkeleton } from "@app/note/components/skeleton";
import { NoteDefaultUser } from "@app/note/components/user/default";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider";
import { READONLY_RELAYS } from "@stores/constants";
import { noteParser } from "@utils/parser";
import { memo, useContext } from "react";
import useSWRSubscription from "swr/subscription";
@ -23,31 +23,22 @@ export const RootNote = memo(function RootNote({
id,
fallback,
}: { id: string; fallback?: any }) {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const parseFallback = isJSON(fallback) ? JSON.parse(fallback) : null;
const { data, error } = useSWRSubscription(
parseFallback ? null : id,
(key, { next }) => {
const unsubscribe = pool.subscribe(
[
{
ids: [key],
},
],
READONLY_RELAYS,
(event: any) => {
next(null, event);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
},
);
const sub = ndk.subscribe({
ids: [key],
});
sub.addListener("event", (event: NDKEvent) => {
next(null, event);
});
return () => {
unsubscribe();
sub.stop();
};
},
);

View File

@ -19,7 +19,7 @@ export function NoteDefaultUser({
<Popover className="relative flex items-start gap-3">
<Popover.Button className="h-11 w-11 shrink-0 overflow-hidden rounded-md bg-zinc-900">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
className="h-14 w-14 shrink-0 rounded-lg object-cover"
/>
<div className="flex w-full flex-1 flex-col gap-2">
<div className="inline-flex w-2/3 flex-col gap-0.5">
<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" />
)}
</h5>

View File

@ -20,7 +20,7 @@ export function NoteQuoteUser({
<div className="group flex items-center gap-2">
<div className="relative h-6 w-6 shrink-0 rounded">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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="relative h-11 w-11 shrink-0 rounded-md">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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.Button className="h-11 w-11 shrink-0 overflow-hidden rounded-md bg-zinc-900">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
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">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
className="h-14 w-14 shrink-0 rounded-lg object-cover"
/>
<div className="flex w-full flex-1 flex-col gap-2">
<div className="inline-flex w-2/3 flex-col gap-0.5">
<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" />
)}
</h5>

View File

@ -2,41 +2,19 @@ import { Kind1 } from "@app/note/components/kind1";
import { NoteMetadata } from "@app/note/components/metadata";
import { RepliesList } from "@app/note/components/replies/list";
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 { noteParser } from "@utils/parser";
import { useContext } from "react";
import useSWRSubscription from "swr/subscription";
import { getNoteByID } from "@utils/storage";
import useSWR from "swr";
const fetcher = ([, id]) => getNoteByID(id);
export function Page() {
const pool: any = useContext(RelayContext);
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const noteID = searchParams.id;
const { data, error } = useSWRSubscription(
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 { data, error } = useSWR(["note", noteID], fetcher);
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 { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from "@utils/date";
import {
addToBlacklist,
@ -9,9 +9,7 @@ import {
createChat,
createNote,
} from "@utils/storage";
import { getParentID } from "@utils/transform";
import { useCallback, useContext, useRef } from "react";
import useSWRSubscription from "swr/subscription";
import { useContext, useEffect, useRef } from "react";
import { navigate } from "vite-plugin-ssr/client/router";
let totalNotes: number;
@ -21,127 +19,69 @@ if (typeof window !== "undefined") {
}
export function Page() {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const now = useRef(new Date());
const [account, lastLogin] = useActiveAccount((state: any) => [
state.account,
state.lastLogin,
]);
const now = useRef(new Date());
const eose = useRef(0);
async function fetchNotes() {
try {
const follows = JSON.parse(account.follows);
let queryNoteSince: number;
const getQuery = useCallback(() => {
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 {
if (totalNotes === 0) {
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
query.push({
kinds: [1, 6],
authors: follows,
since: queryNoteSince,
});
async function fetchChannelBlacklist() {
try {
const filter: NDKFilter = {
authors: [account.pubkey],
kinds: [43, 44],
since: lastLogin,
};
// kind 4 (chats) query
query.push({
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) => {
const events = await ndk.fetchEvents(filter);
events.forEach((event) => {
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:
if (event.tags[0][0] === "e") {
addToBlacklist(account.id, event.tags[0][1], 43, 1);
}
break;
// mute user (channel only)
case 44:
if (event.tags[0][0] === "p") {
addToBlacklist(account.id, event.tags[0][1], 44, 1);
@ -150,37 +90,85 @@ export function Page() {
default:
break;
}
},
undefined,
() => {
eose.current += 1;
if (eose.current === READONLY_RELAYS.length) {
timeout = setTimeout(
() => navigate("/app/space", { overwriteLastHistoryEntry: true }),
2000,
);
}
},
{
unsubscribeOnEose: true,
},
);
});
return () => {
unsubscribe();
clearTimeout(timeout);
};
});
return true;
} catch (e) {
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 (
<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">
{/* dragging area */}
<div
data-tauri-drag-region
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="flex flex-col items-center gap-2">
<LumeIcon className="h-16 w-16 text-black dark:text-white" />

View File

@ -1,24 +1,23 @@
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 { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { open } from "@tauri-apps/api/dialog";
import { Body, fetch } from "@tauri-apps/api/http";
import { createBlobFromFile } from "@utils/createBlobFromFile";
import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
export function AddImageBlock({ parentState }: { parentState: any }) {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const [account, addBlock] = useActiveAccount((state: any) => [
state.account,
state.addBlock,
]);
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true);
const [image, setImage] = useState("");
@ -91,19 +90,19 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
const onSubmit = (data: any) => {
setLoading(true);
const event: any = {
content: data.title,
created_at: dateToUnix(),
kind: 1063,
pubkey: account.pubkey,
tags: tags.current,
};
const signer = new NDKPrivateKeySigner(account.privkey);
ndk.signer = signer;
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event = new NDKEvent(ndk);
// build event
event.content = data.title;
event.kind = 1063;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = tags.current;
// publish channel
pool.publish(event, WRITEONLY_RELAYS);
// publish event
event.publish();
// insert to database
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 { useChannels } from "@stores/channels";
import { useChatMessages, useChats } from "@stores/chats";
import { DEFAULT_AVATAR, READONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { DEFAULT_AVATAR } from "@stores/constants";
import { usePageContext } from "@utils/hooks/usePageContext";
import { useProfile } from "@utils/hooks/useProfile";
import { sendNativeNotification } from "@utils/notification";
import { createNote } from "@utils/storage";
import { useContext } from "react";
import useSWRSubscription from "swr/subscription";
export function ActiveAccount({ data }: { data: any }) {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const pageContext = usePageContext();
@ -35,60 +33,38 @@ export function ActiveAccount({ data }: { data: any }) {
() => {
const follows = JSON.parse(account.follows);
// subscribe to channel
const unsubscribe = pool.subscribe(
[
{
kinds: [1, 6],
authors: follows,
since: dateToUnix(),
},
{
"#p": [data.pubkey],
since: lastLogin,
},
],
READONLY_RELAYS,
(event) => {
switch (event.kind) {
case 1:
case 6: {
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
);
break;
const sub = ndk.subscribe({
"#p": [data.pubkey],
since: lastLogin,
});
sub.addListener("event", (event) => {
switch (event.kind) {
case 4:
if (!isChatPage) {
// save
saveChat(data.pubkey, event);
// update state
notifyChat(event.pubkey);
// send native notifiation
sendNativeNotification("You've received new message");
}
case 4:
if (!isChatPage) {
// save
saveChat(data.pubkey, event);
// update state
notifyChat(event.pubkey);
// send native notifiation
sendNativeNotification("You've received new message");
}
break;
case 42:
if (!isChannelPage) {
// update state
notifyChannel(event);
// send native notifiation
sendNativeNotification(event.content);
}
break;
default:
break;
}
},
);
break;
case 42:
if (!isChannelPage) {
// update state
notifyChannel(event);
// send native notifiation
sendNativeNotification(event.content);
}
break;
default:
break;
}
});
return () => {
unsubscribe();
sub.stop();
};
},
);
@ -96,7 +72,7 @@ export function ActiveAccount({ data }: { data: any }) {
return (
<button type="button" className="relative h-11 w-11 overflow-hidden">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={data.npub}
className="h-11 w-11 rounded-md object-cover"
/>

View File

@ -9,7 +9,7 @@ export function InactiveAccount({ data }: { data: any }) {
return (
<div className="relative h-11 w-11 shrink rounded-md">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={data.npub}
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 { TrashIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { getEventHash, getSignature } from "nostr-tools";
import { useCallback, useContext, useMemo, useState } from "react";
import { Node, Transforms, createEditor } from "slate";
import { withHistory } from "slate-history";
@ -59,12 +58,13 @@ const ImagePreview = ({
};
export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const pool: any = useContext(RelayContext);
const ndk = useContext(RelayContext);
const editor = useMemo(
() => withReact(withImages(withHistory(createEditor()))),
[],
);
const [content, setContent] = useState<Node[]>([
{
children: [
@ -82,20 +82,19 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const submit = () => {
// serialize content
const serializedContent = serialize(content);
console.log(serializedContent);
const event: any = {
content: serializedContent,
created_at: dateToUnix(),
kind: 1,
pubkey: pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = getSignature(event, privkey);
const signer = new NDKPrivateKeySigner(privkey);
ndk.signer = signer;
// publish note
pool.publish(event, WRITEONLY_RELAYS);
const event = new NDKEvent(ndk);
event.kind = 1;
event.content = serializedContent;
event.created_at = dateToUnix();
event.pubkey = pubkey;
event.tags = [];
// publish event
event.publish();
};
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="h-8 w-8 shrink-0 overflow-hidden rounded bg-zinc-900">
<Image
src={user?.picture || DEFAULT_AVATAR}
src={user?.image || DEFAULT_AVATAR}
alt={pubkey}
className="h-8 w-8 object-cover"
loading="auto"

View File

@ -1,16 +1,10 @@
import { FULL_RELAYS } from "@stores/constants";
import { RelayPool } from "nostr-relaypool";
import { initNDK } from "@libs/ndk";
import NDK from "@nostr-dev-kit/ndk";
import { createContext } from "react";
export const RelayContext = createContext({});
const pool = new RelayPool(FULL_RELAYS, {
useEventCache: false,
subscriptionCache: true,
logErrorsAndNotices: false,
logSubscriptions: false,
});
export const RelayContext = createContext<NDK>(null);
const ndk = await initNDK();
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://relay.nostr.band",
"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 { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { createNote, getNoteByID } from "@utils/storage";
import { getParentID } from "@utils/transform";
import { useContext } from "react";
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) {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const ndk = useContext(RelayContext);
const { data } = useSWR(["note", ndk, id], fetcher);
const { data: cache } = useSWR(["event", id], fetcher);
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;
return data;
}

View File

@ -1,12 +1,11 @@
import NDK from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider";
import { METADATA_RELAY } from "@stores/constants";
import { createPleb, getPleb } from "@utils/storage";
import { nip19 } from "nostr-tools";
import { useContext } from "react";
import useSWR from "swr";
import useSWRSubscription from "swr/subscription";
const fetcher = async (key: string) => {
const fetcher = async ([, ndk, key]) => {
let npub: string;
if (key.substring(0, 4) === "npub") {
@ -18,59 +17,27 @@ const fetcher = async (key: string) => {
const current = Math.floor(Date.now() / 1000);
const result = await getPleb(npub);
if (result && result.created_at + 86400 < current) {
if (result && result.created_at + 86400 > current) {
return result;
} else {
return null;
const user = ndk.getUser({ npub });
await user.fetchProfile();
await createPleb(key, user.profile);
return user.profile;
}
};
export function useProfile(key: string) {
const pool: any = useContext(RelayContext);
const {
data: cache,
error,
isLoading,
} = useSWR(key, fetcher, {
const ndk = useContext(RelayContext);
const { data, error, isLoading } = useSWR(["profile", ndk, key], fetcher, {
revalidateIfStale: false,
revalidateOnFocus: false,
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 {
user: newest ? newest : cache,
user: data,
isLoading,
isError: error,
};

View File

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

View File

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