wip: migrate to zustand

This commit is contained in:
Ren Amamiya 2023-05-26 14:45:12 +07:00
parent 5c7b18bf29
commit 671b857077
34 changed files with 494 additions and 530 deletions

View File

@ -37,7 +37,8 @@
"swr": "^2.1.5",
"tailwind-merge": "^1.12.0",
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
"vidstack": "^0.4.5"
"vidstack": "^0.4.5",
"zustand": "^4.3.8"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",

View File

@ -76,6 +76,9 @@ dependencies:
vidstack:
specifier: ^0.4.5
version: 0.4.5
zustand:
specifier: ^4.3.8
version: 4.3.8(react@18.2.0)
devDependencies:
'@tailwindcss/typography':
@ -3082,6 +3085,22 @@ packages:
engines: {node: '>= 14', npm: '>= 7'}
dev: true
/zustand@4.3.8(react@18.2.0):
resolution: {integrity: sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==}
engines: {node: '>=12.7.0'}
peerDependencies:
immer: '>=9.0'
react: '>=16.8'
peerDependenciesMeta:
immer:
optional: true
react:
optional: true
dependencies:
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false

View File

@ -1,8 +1,8 @@
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 { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@ -11,8 +11,10 @@ import { navigate } from "vite-plugin-ssr/client/router";
export function Page() {
const pool: any = useContext(RelayContext);
const { account } = useActiveAccount();
const [account, fetchAccount] = useActiveAccount((state: any) => [
state.account,
state.fetch,
]);
const [image, setImage] = useState(DEFAULT_AVATAR);
const [loading, setLoading] = useState(false);
@ -50,6 +52,10 @@ export function Page() {
);
};
useEffect(() => {
fetchAccount();
}, [fetchAccount]);
useEffect(() => {
setValue("picture", image);
}, [setValue, image]);

View File

@ -1,8 +1,8 @@
import { User } from "@app/auth/components/user";
import CheckCircleIcon from "@icons/checkCircle";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { updateAccount } from "@utils/storage";
import { arrayToNIP02 } from "@utils/transform";
import { getEventHash, getSignature } from "nostr-tools";
@ -111,8 +111,10 @@ const initialList = [
export function Page() {
const pool: any = useContext(RelayContext);
const { account } = useActiveAccount();
const [account, updateFollows] = useActiveAccount((state: any) => [
state.account,
state.updateFollows,
]);
const [loading, setLoading] = useState(false);
const [follows, setFollows] = useState([]);
@ -128,9 +130,12 @@ export function Page() {
const submit = async () => {
setLoading(true);
// update account follows
// update account follows in database
updateAccount("follows", follows, account.pubkey);
// update account follows in state
updateFollows(JSON.stringify(follows));
const tags = arrayToNIP02(follows);
const event: any = {

View File

@ -1,18 +1,19 @@
import { User } from "@app/auth/components/user";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { updateAccount } from "@utils/storage";
import { nip02ToArray } from "@utils/transform";
import { useContext, useState } from "react";
import { useContext, useEffect, useState } from "react";
import useSWRSubscription from "swr/subscription";
import { navigate } from "vite-plugin-ssr/client/router";
export function Page() {
const pool: any = useContext(RelayContext);
const { account } = useActiveAccount();
const [account, fetchAccount, updateFollows] = useActiveAccount(
(state: any) => [state.account, state.fetch, state.updateFollows],
);
const [loading, setLoading] = useState(false);
const [follows, setFollows] = useState([]);
@ -42,9 +43,12 @@ export function Page() {
// follows as list
const followsList = nip02ToArray(follows);
// update account follows
// update account follows in database
updateAccount("follows", followsList, account.pubkey);
// update account follows in store
updateFollows(JSON.stringify(followsList));
// redirect to home
setTimeout(
() => navigate("/app/prefetch", { overwriteLastHistoryEntry: true }),
@ -52,6 +56,10 @@ export function Page() {
);
};
useEffect(() => {
fetchAccount();
}, [fetchAccount]);
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">

View File

@ -1,28 +1,21 @@
import { Dialog, Transition } from "@headlessui/react";
import CancelIcon from "@icons/cancel";
import PlusIcon from "@icons/plus";
import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import CancelIcon from "@icons/cancel";
import PlusIcon from "@icons/plus";
import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { createChannel } from "@utils/storage";
import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useSWRConfig } from "swr";
import { navigate } from "vite-plugin-ssr/client/router";
export default function ChannelCreateModal() {
const pool: any = useContext(RelayContext);
const { account, isError, isLoading } = useActiveAccount();
const { mutate } = useSWRConfig();
const account = useActiveAccount((state: any) => state.account);
const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState(DEFAULT_AVATAR);
@ -47,7 +40,7 @@ export default function ChannelCreateModal() {
const onSubmit = (data: any) => {
setLoading(true);
if (!isError && !isLoading && account) {
if (account) {
const event: any = {
content: JSON.stringify(data),
created_at: dateToUnix(),
@ -62,8 +55,6 @@ export default function ChannelCreateModal() {
pool.publish(event, WRITEONLY_RELAYS);
// insert to database
createChannel(event.id, event.pubkey, event.content, event.created_at);
// update channe llist
mutate("channels");
// reset form
reset();
setTimeout(() => {

View File

@ -1,18 +1,19 @@
import ChannelCreateModal from "@app/channel/components/createModal";
import ChannelsListItem from "@app/channel/components/item";
import { getChannels } from "@utils/storage";
import useSWR from "swr";
const fetcher = () => getChannels(10, 0);
import { useChannels } from "@stores/channels";
import { useEffect } from "react";
export default function ChannelsList() {
const { data, error }: any = useSWR("channels", fetcher);
const channels = useChannels((state: any) => state.channels);
const fetchChannels = useChannels((state: any) => state.fetch);
useEffect(() => {
fetchChannels();
}, [fetchChannels]);
return (
<div className="flex flex-col gap-1">
{!data || error ? (
{!channels ? (
<>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
@ -24,7 +25,7 @@ export default function ChannelsList() {
</div>
</>
) : (
data.map((item: { event_id: string }) => (
channels.map((item: { event_id: string }) => (
<ChannelsListItem key={item.event_id} data={item} />
))
)}

View File

@ -1,16 +1,11 @@
import UserReply from "@app/channel/components/messages/userReply";
import CancelIcon from "@icons/cancel";
import { ImagePicker } from "@shared/form/imagePicker";
import { RelayContext } from "@shared/relayProvider";
import CancelIcon from "@icons/cancel";
import { useActiveAccount } from "@stores/accounts";
import { channelContentAtom, channelReplyAtom } from "@stores/channel";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtom, useAtomValue } from "jotai";
import { useResetAtom } from "jotai/utils";
import { getEventHash, getSignature } from "nostr-tools";
@ -20,7 +15,7 @@ export default function ChannelMessageForm({
channelID,
}: { channelID: string | string[] }) {
const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const [value, setValue] = useAtom(channelContentAtom);
const resetValue = useResetAtom(channelContentAtom);
@ -41,7 +36,7 @@ export default function ChannelMessageForm({
tags = [["e", channelID, "", "root"]];
}
if (!isError && !isLoading && account) {
if (account) {
const event: any = {
content: value,
created_at: dateToUnix(),
@ -49,11 +44,13 @@ export default function ChannelMessageForm({
pubkey: account.pubkey,
tags: tags,
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset state
resetValue();
// reset channel reply

View File

@ -1,23 +1,19 @@
import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip";
import { Dialog, Transition } from "@headlessui/react";
import CancelIcon from "@icons/cancel";
import HideIcon from "@icons/hide";
import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip";
import { useActiveAccount } from "@stores/accounts";
import { channelMessagesAtom } from "@stores/channel";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from "jotai";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useState } from "react";
export default function MessageHideButton({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const { account, isError, isLoading } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useAtom(channelMessagesAtom);
@ -31,7 +27,7 @@ export default function MessageHideButton({ id }: { id: string }) {
};
const hideMessage = () => {
if (!isError && !isLoading && account) {
if (account) {
const event: any = {
content: "",
created_at: dateToUnix(),
@ -44,13 +40,16 @@ export default function MessageHideButton({ id }: { id: string }) {
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// update local state
const cloneMessages = [...messages];
const targetMessage = cloneMessages.findIndex(
(message) => message.id === id,
);
cloneMessages[targetMessage]["hide"] = true;
setMessages(cloneMessages);
// close modal
closeModal();
} else {

View File

@ -1,23 +1,19 @@
import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip";
import { Dialog, Transition } from "@headlessui/react";
import CancelIcon from "@icons/cancel";
import MuteIcon from "@icons/mute";
import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip";
import { useActiveAccount } from "@stores/accounts";
import { channelMessagesAtom } from "@stores/channel";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from "jotai";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useState } from "react";
export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
const pool: any = useContext(RelayContext);
const { account, isError, isLoading } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const [messages, setMessages] = useAtom(channelMessagesAtom);
const [isOpen, setIsOpen] = useState(false);
@ -31,7 +27,7 @@ export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
};
const muteUser = () => {
if (!isError && !isLoading && account) {
if (account) {
const event: any = {
content: "",
created_at: dateToUnix(),
@ -44,6 +40,7 @@ export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// update local state
const cloneMessages = [...messages];
const finalMessages = cloneMessages.filter(

View File

@ -1,24 +1,20 @@
import { Dialog, Transition } from "@headlessui/react";
import CancelIcon from "@icons/cancel";
import EditIcon from "@icons/edit";
import { AvatarUploader } from "@shared/avatarUploader";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import CancelIcon from "@icons/cancel";
import EditIcon from "@icons/edit";
import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getChannel } from "@utils/storage";
import { Dialog, Transition } from "@headlessui/react";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
export default function ChannelUpdateModal({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const { account, isError, isLoading } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState(DEFAULT_AVATAR);
@ -52,7 +48,7 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
const onSubmit = (data: any) => {
setLoading(true);
if (!isError && !isLoading && account) {
if (account) {
const event: any = {
content: JSON.stringify(data),
created_at: dateToUnix(),
@ -60,11 +56,13 @@ export default function ChannelUpdateModal({ id }: { id: string }) {
pubkey: account.pubkey,
tags: [["e", id]],
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// publish channel
pool.publish(event, WRITEONLY_RELAYS);
// reset form
reset();
// close modal

View File

@ -3,18 +3,14 @@ import ChannelMembers from "@app/channel/components/members";
import ChannelMessageForm from "@app/channel/components/messages/form";
import ChannelMetadata from "@app/channel/components/metadata";
import ChannelUpdateModal from "@app/channel/components/updateModal";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { channelMessagesAtom, channelReplyAtom } from "@stores/channel";
import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { usePageContext } from "@utils/hooks/usePageContext";
import { getActiveBlacklist, getBlacklist } from "@utils/storage";
import { arrayObjToPureArr } from "@utils/transform";
import { useSetAtom } from "jotai";
import { useResetAtom } from "jotai/utils";
import { Suspense, lazy, useContext, useEffect, useRef } from "react";
@ -39,19 +35,20 @@ const ChannelMessageList = lazy(
export function Page() {
const pool: any = useContext(RelayContext);
const account: any = useActiveAccount((state: any) => state.account);
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const channelID = searchParams.id;
const channelPubkey = searchParams.channelpub;
const { account, isLoading, isError } = useActiveAccount();
const { data: muted } = useSWR(
!isLoading && !isError && account ? ["muted", account.id] : null,
account ? ["muted", account.id] : null,
fetchMuted,
);
const { data: hided } = useSWR(
!isLoading && !isError && account ? ["hided", account.id] : null,
account ? ["hided", account.id] : null,
fetchHided,
);
@ -118,7 +115,7 @@ export function Page() {
<div className="flex items-center gap-2">
<ChannelMembers />
{!muted ? <></> : <ChannelBlackList blacklist={muted.original} />}
{!isLoading && !isError && account ? (
{account ? (
account.pubkey === channelPubkey && (
<ChannelUpdateModal id={channelID} />
)

View File

@ -1,25 +1,23 @@
import ChatsListItem from "@app/chat/components/item";
import ChatsListSelfItem from "@app/chat/components/self";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getChatsByPubkey } from "@utils/storage";
import useSWR from "swr";
const fetcher = ([, pubkey]) => getChatsByPubkey(pubkey);
import { useActiveAccount } from "@stores/accounts";
import { useChats } from "@stores/chats";
import { useEffect } from "react";
export default function ChatsList() {
const { account, isLoading, isError } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const chats = useChats((state: any) => state.chats);
const fetchChats = useChats((state: any) => state.fetch);
const { data: chats, error }: any = useSWR(
!isLoading && !isError && account ? ["chats", account.pubkey] : null,
fetcher,
);
useEffect(() => {
if (!account) return;
fetchChats(account.pubkey);
}, [fetchChats]);
return (
<div className="flex flex-col gap-1">
<ChatsListSelfItem />
{!chats || error ? (
{!chats ? (
<>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />

View File

@ -1,15 +1,12 @@
import { ChatMessageItem } from "@app/chat/components/messages/item";
import { useActiveAccount } from "@stores/accounts";
import { sortedChatMessagesAtom } from "@stores/chat";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtomValue } from "jotai";
import { useCallback, useRef } from "react";
import { Virtuoso } from "react-virtuoso";
export default function ChatMessageList() {
const { account } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const virtuosoRef = useRef(null);
const data = useAtomValue(sortedChatMessagesAtom);

View File

@ -1,12 +1,9 @@
import { ImagePicker } from "@shared/form/imagePicker";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { chatContentAtom } from "@stores/chat";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useAtom } from "jotai";
import { useResetAtom } from "jotai/utils";
import { getEventHash, getSignature, nip04 } from "nostr-tools";
@ -16,7 +13,7 @@ export default function ChatMessageForm({
receiverPubkey,
}: { receiverPubkey: string }) {
const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const [value, setValue] = useAtom(chatContentAtom);
const resetValue = useResetAtom(chatContentAtom);
@ -29,25 +26,23 @@ export default function ChatMessageForm({
);
const submitEvent = () => {
if (!isError && !isLoading && account) {
encryptMessage(account.privkey)
.then((encryptedContent) => {
const event: any = {
content: encryptedContent,
created_at: dateToUnix(),
kind: 4,
pubkey: account.pubkey,
tags: [["p", receiverPubkey]],
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset state
resetValue();
})
.catch(console.error);
}
encryptMessage(account.privkey)
.then((encryptedContent) => {
const event: any = {
content: encryptedContent,
created_at: dateToUnix(),
kind: 4,
pubkey: account.pubkey,
tags: [["p", receiverPubkey]],
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset state
resetValue();
})
.catch(console.error);
};
const handleEnterPress = (e) => {

View File

@ -1,11 +1,8 @@
import { Image } from "@shared/image";
import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { usePageContext } from "@utils/hooks/usePageContext";
import { shortenKey } from "@utils/shortenKey";
import { twMerge } from "tailwind-merge";
export default function ChatsListSelfItem() {
@ -14,12 +11,11 @@ export default function ChatsListSelfItem() {
const searchParams: any = pageContext.urlParsed.search;
const pagePubkey = searchParams.pubkey;
const { account, isLoading, isError } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
return (
<>
{isError && <div>error</div>}
{isLoading && !account ? (
{!account ? (
<div className="inline-flex h-8 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div>

View File

@ -1,13 +1,9 @@
import ChatMessageForm from "@app/chat/components/messages/form";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { chatMessagesAtom } from "@stores/chat";
import { READONLY_RELAYS } from "@stores/constants";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { usePageContext } from "@utils/hooks/usePageContext";
import { useSetAtom } from "jotai";
import { useResetAtom } from "jotai/utils";
import { Suspense, lazy, useContext, useEffect } from "react";
@ -17,13 +13,12 @@ const ChatMessageList = lazy(() => import("@app/chat/components/messageList"));
export function Page() {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const pubkey = searchParams.pubkey;
const { account } = useActiveAccount();
const setChatMessages = useSetAtom(chatMessagesAtom);
const resetChatMessages = useResetAtom(chatMessagesAtom);

View File

@ -1,17 +1,23 @@
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { useActiveAccount } from "@stores/accounts";
import { useEffect } from "react";
import { navigate } from "vite-plugin-ssr/client/router";
export function Page() {
const { account, isLoading } = useActiveAccount();
const fetchAccount = useActiveAccount((state: any) => state.fetch);
const account = useActiveAccount((state: any) => state.account);
if (!isLoading && !account) {
if (!account) {
navigate("/app/auth", { overwriteLastHistoryEntry: true });
}
if (!isLoading && account) {
if (account) {
navigate("/app/prefetch", { overwriteLastHistoryEntry: true });
}
useEffect(() => {
fetchAccount();
}, [fetchAccount]);
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white" />
);

View File

@ -1,12 +1,8 @@
import { RelayContext } from "@shared/relayProvider";
import LikeIcon from "@icons/like";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react";
@ -16,33 +12,31 @@ export default function NoteLike({
likes,
}: { id: string; pubkey: string; likes: number }) {
const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const [count, setCount] = useState(0);
const submitEvent = (e: any) => {
e.stopPropagation();
if (!isLoading && !isError && account) {
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);
} else {
console.log("error");
}
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(() => {

View File

@ -1,14 +1,10 @@
import { Dialog, Transition } from "@headlessui/react";
import ReplyIcon from "@icons/reply";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import ReplyIcon from "@icons/reply";
import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from "@headlessui/react";
import { compactNumber } from "@utils/number";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useEffect, useState } from "react";
@ -18,13 +14,12 @@ export default function NoteReply({
replies,
}: { id: string; replies: number }) {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState("");
const { account, isLoading, isError } = useActiveAccount();
const closeModal = () => {
setIsOpen(false);
};
@ -34,25 +29,24 @@ export default function NoteReply({
};
const submitEvent = () => {
if (!isLoading && !isError && account) {
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [["e", id]],
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [["e", id]],
};
// publish event
pool.publish(event, WRITEONLY_RELAYS);
// close modal
setIsOpen(false);
setCount(count + 1);
} else {
console.log("error");
}
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// publish event
pool.publish(event, WRITEONLY_RELAYS);
// close modal
setIsOpen(false);
// increment replies
setCount(count + 1);
};
useEffect(() => {

View File

@ -1,12 +1,8 @@
import { RelayContext } from "@shared/relayProvider";
import RepostIcon from "@icons/repost";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { compactNumber } from "@utils/number";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useEffect, useState } from "react";
@ -17,33 +13,32 @@ export default function NoteRepost({
reposts,
}: { id: string; pubkey: string; reposts: number }) {
const pool: any = useContext(RelayContext);
const { account, isLoading, isError } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const [count, setCount] = useState(0);
const submitEvent = (e: any) => {
e.stopPropagation();
if (!isLoading && !isError && account) {
const event: any = {
content: "",
kind: 6,
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);
} else {
console.log("error");
}
const event: any = {
content: "",
kind: 6,
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(() => {

View File

@ -1,39 +1,34 @@
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 { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext, useState } from "react";
export default function NoteReplyForm({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const { account, isLoading, isError } = useActiveAccount();
const [value, setValue] = useState("");
const submitEvent = () => {
if (!isLoading && !isError && account) {
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [["e", id]],
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [["e", id]],
};
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset form
setValue("");
} else {
console.log("error");
}
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset form
setValue("");
};
return (

View File

@ -1,8 +1,8 @@
import LumeIcon from "@icons/lume";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { dateToUnix, getHourAgo } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import {
addToBlacklist,
countTotalNotes,
@ -15,15 +15,6 @@ import { useCallback, useContext, useRef } from "react";
import useSWRSubscription from "swr/subscription";
import { navigate } from "vite-plugin-ssr/client/router";
function isJSON(str: string) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
let lastLogin: string;
let totalNotes: number;
@ -34,12 +25,11 @@ if (typeof window !== "undefined") {
export function Page() {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const now = useRef(new Date());
const eose = useRef(0);
const { account, isLoading, isError } = useActiveAccount();
const getQuery = useCallback(() => {
const query = [];
const follows = JSON.parse(account.follows);
@ -79,98 +69,95 @@ export function Page() {
});
return query;
}, [account.follows]);
}, [account]);
useSWRSubscription(
!isLoading && !isError && account ? "prefetch" : null,
() => {
const query = getQuery();
const unsubscribe = pool.subscribe(
query,
READONLY_RELAYS,
(event: any) => {
switch (event.kind) {
// short text note
case 1: {
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,
parentID,
);
break;
useSWRSubscription(account ? "prefetch" : null, () => {
const query = getQuery();
const unsubscribe = pool.subscribe(
query,
READONLY_RELAYS,
(event: any) => {
switch (event.kind) {
// short text note
case 1: {
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,
parentID,
);
break;
}
// chat
case 4:
createChat(
event.id,
account.pubkey,
event.pubkey,
event.content,
event.created_at,
);
break;
// repost
case 6:
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
event.id,
);
break;
// hide message (channel only)
case 43:
if (event.tags[0][0] === "e") {
addToBlacklist(account.id, event.tags[0][1], 43, 1);
}
// chat
case 4:
createChat(
event.id,
account.pubkey,
event.pubkey,
event.content,
event.created_at,
);
break;
// repost
case 6:
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
event.id,
);
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);
}
break;
case 1063:
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
"",
);
break;
default:
break;
}
},
undefined,
() => {
eose.current += 1;
if (eose.current === READONLY_RELAYS.length) {
navigate("/app/space", { overwriteLastHistoryEntry: true });
}
},
);
break;
// mute user (channel only)
case 44:
if (event.tags[0][0] === "p") {
addToBlacklist(account.id, event.tags[0][1], 44, 1);
}
break;
case 1063:
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
"",
);
break;
default:
break;
}
},
undefined,
() => {
eose.current += 1;
if (eose.current === READONLY_RELAYS.length) {
navigate("/app/space", { overwriteLastHistoryEntry: true });
}
},
);
return () => {
unsubscribe();
};
},
);
return () => {
unsubscribe();
};
});
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">

View File

@ -3,19 +3,15 @@ import { Dialog, Transition } from "@headlessui/react";
import CancelIcon from "@icons/cancel";
import PlusIcon from "@icons/plus";
import { Image } from "@shared/image";
import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { createBlock, getPlebs } from "@utils/storage";
import { createBlock } from "@utils/storage";
import { Fragment, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import useSWR from "swr";
const fetcher = () => getPlebs();
export function CreateBlockModal() {
const { account } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const { register, handleSubmit, reset, watch, setValue } = useForm();
const { data: plebs } = useSWR("plebs", fetcher);
const [image, setImage] = useState(DEFAULT_AVATAR);
const [isOpen, setIsOpen] = useState(false);

View File

@ -1,24 +1,18 @@
import { Post } from "@shared/composer/types/post";
import { User } from "@shared/composer/user";
import { Dialog, Transition } from "@headlessui/react";
import CancelIcon from "@icons/cancel";
import ChevronDownIcon from "@icons/chevronDown";
import ChevronRightIcon from "@icons/chevronRight";
import ComposeIcon from "@icons/compose";
import { composerAtom } from "@stores/composer";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { Dialog, Transition } from "@headlessui/react";
import { useAtom } from "jotai";
import { Post } from "@shared/composer/types/post";
import { User } from "@shared/composer/user";
import { useActiveAccount } from "@stores/accounts";
import { Fragment, useState } from "react";
export function ComposerModal() {
const [isOpen, setIsOpen] = useState(false);
const [composer] = useAtom(composerAtom);
const [composer] = useState({ type: "post" });
const { account, isLoading, isError } = useActiveAccount();
const account = useActiveAccount((state: any) => state.account);
const closeModal = () => {
setIsOpen(false);
@ -64,11 +58,7 @@ export function ComposerModal() {
<Dialog.Panel className="relative h-min w-full max-w-xl rounded-lg border border-zinc-800 bg-zinc-900">
<div className="flex items-center justify-between px-4 py-4">
<div className="flex items-center gap-2">
<div>
{!isLoading && !isError && account && (
<User data={account} />
)}
</div>
<div>{account && <User data={account} />}</div>
<span>
<ChevronRightIcon
width={14}

View File

@ -1,141 +1,119 @@
import { RelayContext } from "@shared/relayProvider";
import HeartBeatIcon from "@icons/heartbeat";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { READONLY_RELAYS } from "@stores/constants";
import { hasNewerNoteAtom } from "@stores/note";
import { TauriEvent } from "@tauri-apps/api/event";
import { appWindow, getCurrent } from "@tauri-apps/api/window";
import { dateToUnix } from "@utils/date";
import { useActiveAccount } from "@utils/hooks/useActiveAccount";
import { createChat, createNote, updateAccount } from "@utils/storage";
import {
createChat,
createNote,
updateAccount,
updateLastLogin,
} from "@utils/storage";
import { getParentID, nip02ToArray } from "@utils/transform";
import { useSetAtom } from "jotai";
import { useContext, useRef } from "react";
import { useContext, useEffect, useRef } from "react";
import useSWRSubscription from "swr/subscription";
function isJSON(str: string) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
export default function EventCollector() {
const pool: any = useContext(RelayContext);
const setHasNewerNote = useSetAtom(hasNewerNoteAtom);
const account = useActiveAccount((state: any) => state.account);
const now = useRef(new Date());
const { account, isLoading, isError } = useActiveAccount();
useSWRSubscription(
!isLoading && !isError && account ? ["eventCollector", account] : null,
([, key]) => {
const follows = JSON.parse(key.follows);
const followsAsArray = nip02ToArray(follows);
const unsubscribe = pool.subscribe(
[
{
kinds: [1, 6],
authors: followsAsArray,
since: dateToUnix(now.current),
},
{
kinds: [3],
authors: [key.pubkey],
},
{
kinds: [4],
"#p": [key.pubkey],
since: dateToUnix(now.current),
},
{
kinds: [30023],
since: dateToUnix(now.current),
},
],
READONLY_RELAYS,
(event: any) => {
switch (event.kind) {
// short text note
case 1: {
const parentID = getParentID(event.tags, event.id);
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
parentID,
);
// notify user reload to get newer note
setHasNewerNote(true);
break;
}
// contacts
case 3: {
const follows = nip02ToArray(event.tags);
// update account's folllows with NIP-02 tag list
updateAccount("follows", follows, event.pubkey);
break;
}
// chat
case 4:
createChat(
event.id,
key.pubkey,
event.pubkey,
event.content,
event.created_at,
);
break;
// repost
case 6:
createNote(
event.id,
key.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
event.id,
);
break;
// long post
case 30023: {
const verifyMetadata = isJSON(event.tags);
if (verifyMetadata) {
// insert event to local database
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
"",
);
}
break;
}
default:
break;
}
useSWRSubscription(account ? "eventCollector" : null, () => {
const follows = JSON.parse(account.follows);
const unsubscribe = pool.subscribe(
[
{
kinds: [1, 6],
authors: follows,
since: dateToUnix(now.current),
},
);
{
kinds: [3],
authors: [account.pubkey],
},
{
kinds: [4],
"#p": [account.pubkey],
since: dateToUnix(now.current),
},
],
READONLY_RELAYS,
(event: any) => {
switch (event.kind) {
// short text note
case 1: {
const parentID = getParentID(event.tags, event.id);
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
parentID,
);
// notify user reload to get newer note
setHasNewerNote(true);
break;
}
// contacts
case 3: {
const follows = nip02ToArray(event.tags);
// update account's folllows with NIP-02 tag list
updateAccount("follows", follows, event.pubkey);
break;
}
// chat
case 4:
createChat(
event.id,
account.pubkey,
event.pubkey,
event.content,
event.created_at,
);
break;
// repost
case 6:
createNote(
event.id,
account.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
event.id,
);
break;
default:
break;
}
},
);
return () => {
unsubscribe();
};
},
);
return () => {
unsubscribe();
};
});
useEffect(() => {
// listen window close event
getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
// update last login time
updateLastLogin(dateToUnix(now.current));
// close window
appWindow.close();
});
}, []);
return (
<div className="inline-flex h-6 w-6 items-center justify-center rounded text-zinc-500 hover:bg-zinc-900 hover:text-green-500">

13
src/stores/accounts.tsx Normal file
View File

@ -0,0 +1,13 @@
import { getActiveAccount } from "@utils/storage";
import { create } from "zustand";
export const useActiveAccount = create((set) => ({
account: null,
fetch: async () => {
const response = await getActiveAccount();
set({ account: response });
},
updateFollows: (list: any) => {
set((state: any) => ({ account: { ...state.account, follows: list } }));
},
}));

17
src/stores/channels.tsx Normal file
View File

@ -0,0 +1,17 @@
import { getChannels } from "@utils/storage";
import { create } from "zustand";
export const useChannels = create((set) => ({
channels: [],
fetch: async () => {
const response = await getChannels(10, 0);
set({ channels: response });
},
}));
export const useChannelMessage = create((set) => ({
messages: [],
add: (message: any) => {
set((state: any) => ({ messages: [...state.messages, message] }));
},
}));

21
src/stores/chats.tsx Normal file
View File

@ -0,0 +1,21 @@
import { getChatMessages, getChatsByPubkey } from "@utils/storage";
import { create } from "zustand";
export const useChats = create((set) => ({
chats: [],
fetch: async (pubkey: string) => {
const response = await getChatsByPubkey(pubkey);
set({ chats: response });
},
}));
export const useChatMessages = create((set) => ({
messages: [],
fetch: async (receiver_pubkey: string, sender_pubkey: string) => {
const response = await getChatMessages(receiver_pubkey, sender_pubkey);
set({ messages: response });
},
add: (message: any) => {
set((state: any) => ({ messages: [...state.messages, message] }));
},
}));

View File

@ -1,3 +0,0 @@
import { atom } from "jotai";
export const composerAtom = atom({ type: "post" });

View File

@ -1,8 +0,0 @@
import { atom } from "jotai";
import { atomWithReset } from "jotai/utils";
// note content
export const noteContentAtom = atomWithReset("");
// notify user that connector has receive newer note
export const hasNewerNoteAtom = atom(false);

View File

@ -1,8 +0,0 @@
import { atom } from "jotai";
export const onboardingAtom = atom({
pubkey: null,
privkey: null,
metadata: null,
follows: null,
});

View File

@ -1,14 +0,0 @@
import { getActiveAccount } from "@utils/storage";
import useSWR from "swr";
const fetcher = () => getActiveAccount();
export function useActiveAccount() {
const { data, error, isLoading } = useSWR("activeAcount", fetcher);
return {
account: data,
isLoading,
isError: error,
};
}

View File

@ -272,7 +272,7 @@ export async function updateChannelMetadata(event_id: string, value: string) {
);
}
// get all chats
// get all chats by pubkey
export async function getChatsByPubkey(pubkey: string) {
const db = await connect();
return await db.select(
@ -280,6 +280,17 @@ export async function getChatsByPubkey(pubkey: string) {
);
}
// get chat messages
export async function getChatMessages(
receiver_pubkey: string,
sender_pubkey: string,
) {
const db = await connect();
return await db.select(
`SELECT * FROM chats WHERE receiver_pubkey = "${receiver_pubkey}" AND sender_pubkey = "${sender_pubkey}" ORDER BY created_at ASC;`,
);
}
// create chat
export async function createChat(
event_id: string,