diff --git a/src/components/chats/chatList.tsx b/src/components/chats/chatList.tsx
new file mode 100644
index 00000000..d3b9c2f2
--- /dev/null
+++ b/src/components/chats/chatList.tsx
@@ -0,0 +1,44 @@
+import { ImageWithFallback } from '@components/imageWithFallback';
+
+import { activeAccountAtom } from '@stores/account';
+import { DEFAULT_AVATAR } from '@stores/constants';
+
+import { useAtomValue } from 'jotai';
+import { useRouter } from 'next/router';
+
+export default function ChatList() {
+ const router = useRouter();
+
+ const activeAccount: any = useAtomValue(activeAccountAtom);
+ const accountProfile = JSON.parse(activeAccount.metadata);
+
+ const openChats = () => {
+ router.push({
+ pathname: '/chats/[pubkey]',
+ query: { pubkey: activeAccount.pubkey },
+ });
+ };
+
+ return (
+
+
openChats()}
+ className="inline-flex items-center gap-2 rounded-md px-2.5 py-2 hover:bg-zinc-900"
+ >
+
+
+
+
+
+ {accountProfile.display_name || accountProfile.name} (you)
+
+
+
+
+ );
+}
diff --git a/src/components/chats/message.tsx b/src/components/chats/message.tsx
new file mode 100644
index 00000000..c17b4fb9
--- /dev/null
+++ b/src/components/chats/message.tsx
@@ -0,0 +1,49 @@
+import { MessageUser } from '@components/chats/user';
+
+import { nip04 } from 'nostr-tools';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+export const Message = ({
+ data,
+ activeAccountPubkey,
+ activeAccountPrivkey,
+}: {
+ data: any;
+ activeAccountPubkey: string;
+ activeAccountPrivkey: string;
+}) => {
+ const [content, setContent] = useState('');
+
+ const sender = useMemo(() => {
+ const pTag = data.tags.find(([k, v]) => k === 'p' && v && v !== '')[1];
+ if (pTag === activeAccountPubkey) {
+ return data.pubkey;
+ } else {
+ return pTag;
+ }
+ }, [data.pubkey, data.tags, activeAccountPubkey]);
+
+ const decryptContent = useCallback(async () => {
+ const result = await nip04.decrypt(activeAccountPrivkey, sender, data.content);
+ setContent(result);
+ }, [data.content, activeAccountPrivkey, sender]);
+
+ useEffect(() => {
+ decryptContent().catch(console.error);
+ }, [decryptContent]);
+
+ return (
+
+ );
+};
diff --git a/src/components/chats/messageList.tsx b/src/components/chats/messageList.tsx
new file mode 100644
index 00000000..6ad400ad
--- /dev/null
+++ b/src/components/chats/messageList.tsx
@@ -0,0 +1,44 @@
+import { Message } from '@components/chats/message';
+
+import { useCallback, useRef } from 'react';
+import { Virtuoso } from 'react-virtuoso';
+
+export const MessageList = ({ data }: { data: any }) => {
+ const virtuosoRef = useRef(null);
+
+ const itemContent: any = useCallback(
+ (index: string | number) => {
+ const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
+ return (
+
+ );
+ },
+ [data]
+ );
+
+ const computeItemKey = useCallback(
+ (index: string | number) => {
+ return data[index].id;
+ },
+ [data]
+ );
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/chats/user.tsx b/src/components/chats/user.tsx
new file mode 100644
index 00000000..9ac89062
--- /dev/null
+++ b/src/components/chats/user.tsx
@@ -0,0 +1,37 @@
+import { ImageWithFallback } from '@components/imageWithFallback';
+
+import { DEFAULT_AVATAR } from '@stores/constants';
+
+import { useMetadata } from '@utils/metadata';
+import { truncate } from '@utils/truncate';
+
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+dayjs.extend(relativeTime);
+
+export const MessageUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
+ const profile = useMetadata(pubkey);
+
+ return (
+
+
+
+
+
+
+
+ {profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
+
+ ยท
+ {dayjs().to(dayjs.unix(time))}
+
+
+
+ );
+};
diff --git a/src/components/form/chat.tsx b/src/components/form/chat.tsx
new file mode 100644
index 00000000..6681b1d2
--- /dev/null
+++ b/src/components/form/chat.tsx
@@ -0,0 +1,79 @@
+import ImagePicker from '@components/form/imagePicker';
+import { RelayContext } from '@components/relaysProvider';
+
+import { dateToUnix } from '@utils/getDate';
+
+import { getEventHash, nip04, signEvent } from 'nostr-tools';
+import { useCallback, useContext, useState } from 'react';
+
+export default function FormChat({ receiverPubkey }: { receiverPubkey: string }) {
+ const [pool, relays]: any = useContext(RelayContext);
+ const [value, setValue] = useState('');
+
+ const encryptMessage = useCallback(
+ async (privkey: string) => {
+ return await nip04.encrypt(privkey, receiverPubkey, value);
+ },
+ [receiverPubkey, value]
+ );
+
+ const submitEvent = useCallback(() => {
+ const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
+ encryptMessage(activeAccount.privkey)
+ .then((encryptedContent) => {
+ const event: any = {
+ content: encryptedContent,
+ created_at: dateToUnix(),
+ kind: 4,
+ pubkey: activeAccount.pubkey,
+ tags: [['p', receiverPubkey]],
+ };
+ event.id = getEventHash(event);
+ event.sig = signEvent(event, activeAccount.privkey);
+ // publish note
+ pool.publish(event, relays);
+ // reset state
+ setValue('');
+ })
+ .catch(console.error);
+ }, [encryptMessage, receiverPubkey, pool, relays]);
+
+ const handleEnterPress = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ submitEvent();
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/navigation/channels.tsx b/src/components/navigation/channels.tsx
index 23ce19b9..834e08bf 100644
--- a/src/components/navigation/channels.tsx
+++ b/src/components/navigation/channels.tsx
@@ -7,16 +7,16 @@ export default function Channels() {
return (
-
-
+
diff --git a/src/components/navigation/chats.tsx b/src/components/navigation/chats.tsx
index aa2a947d..27270179 100644
--- a/src/components/navigation/chats.tsx
+++ b/src/components/navigation/chats.tsx
@@ -1,3 +1,5 @@
+import ChatList from '@components/chats/chatList';
+
import * as Collapsible from '@radix-ui/react-collapsible';
import { TriangleUpIcon } from '@radix-ui/react-icons';
import { useState } from 'react';
@@ -7,18 +9,20 @@ export default function Chats() {
return (
-
-
+
+
- Chats
+ Chats
-
+
+
+
);
diff --git a/src/components/navigation/newsfeed.tsx b/src/components/navigation/newsfeed.tsx
index aa46ff43..c1d0f813 100644
--- a/src/components/navigation/newsfeed.tsx
+++ b/src/components/navigation/newsfeed.tsx
@@ -9,8 +9,8 @@ export default function Newsfeed() {
return (
-
-
+
+
- Newsfeed
+ Newsfeed
-
-
-
Following
-
-
-
Circle
diff --git a/src/components/user/extend.tsx b/src/components/user/extend.tsx
index bc8bd7cd..2ba60143 100644
--- a/src/components/user/extend.tsx
+++ b/src/components/user/extend.tsx
@@ -16,12 +16,12 @@ export const UserExtend = ({ pubkey, time }: { pubkey: string; time: number }) =
return (
-
+
diff --git a/src/components/user/large.tsx b/src/components/user/large.tsx
index d896e379..9986f1bc 100644
--- a/src/components/user/large.tsx
+++ b/src/components/user/large.tsx
@@ -16,7 +16,7 @@ export const UserLarge = ({ pubkey, time }: { pubkey: string; time: number }) =>
return (
-
+
{
const profile = useMetadata(pubkey);
-
- return @{profile?.name || truncate(pubkey, 16, ' .... ')};
-});
+ return (
+
+ @{profile?.name || profile?.username || truncate(pubkey, 16, ' .... ')}
+
+ );
+};
diff --git a/src/components/user/mini.tsx b/src/components/user/mini.tsx
deleted file mode 100644
index 5f2a7505..00000000
--- a/src/components/user/mini.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ImageWithFallback } from '@components/imageWithFallback';
-
-import { DEFAULT_AVATAR } from '@stores/constants';
-
-import { useMetadata } from '@utils/metadata';
-import { truncate } from '@utils/truncate';
-
-export const UserMini = ({ pubkey }: { pubkey: string }) => {
- const profile = useMetadata(pubkey);
-
- return (
-
-
-
-
-
-
- {profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
-
-
-
- );
-};
diff --git a/src/pages/chats/[pubkey].tsx b/src/pages/chats/[pubkey].tsx
new file mode 100644
index 00000000..fe863c63
--- /dev/null
+++ b/src/pages/chats/[pubkey].tsx
@@ -0,0 +1,82 @@
+import BaseLayout from '@layouts/base';
+import WithSidebarLayout from '@layouts/withSidebar';
+
+import { MessageList } from '@components/chats/messageList';
+import FormChat from '@components/form/chat';
+import { RelayContext } from '@components/relaysProvider';
+
+import { activeAccountAtom } from '@stores/account';
+
+import { useAtomValue } from 'jotai';
+import { useRouter } from 'next/router';
+import {
+ JSXElementConstructor,
+ ReactElement,
+ ReactFragment,
+ ReactPortal,
+ useContext,
+ useEffect,
+ useState,
+} from 'react';
+
+export default function Page() {
+ const [pool, relays]: any = useContext(RelayContext);
+
+ const router = useRouter();
+ const pubkey: any = router.query.pubkey || null;
+
+ const activeAccount: any = useAtomValue(activeAccountAtom);
+ const [messages, setMessages] = useState([]);
+
+ useEffect(() => {
+ const unsubscribe = pool.subscribe(
+ [
+ {
+ kinds: [4],
+ authors: [pubkey],
+ '#p': [activeAccount.pubkey],
+ since: 0,
+ },
+ {
+ kinds: [4],
+ authors: [activeAccount.pubkey],
+ '#p': [pubkey],
+ since: 0,
+ },
+ ],
+ relays,
+ (event: any) => {
+ setMessages((messages) => [event, ...messages]);
+ }
+ );
+
+ return () => {
+ unsubscribe;
+ };
+ }, [pool, relays, pubkey, activeAccount.pubkey]);
+
+ return (
+
+
a.created_at - b.created_at)} />
+
+
+
+
+ );
+}
+
+Page.getLayout = function getLayout(
+ page:
+ | string
+ | number
+ | boolean
+ | ReactElement>
+ | ReactFragment
+ | ReactPortal
+) {
+ return (
+
+ {page}
+
+ );
+};