126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { useCallback, useEffect, useRef } from 'react';
|
|
import { useParams } from 'react-router-dom';
|
|
import { VList, VListHandle } from 'virtua';
|
|
|
|
import { ChatForm } from '@app/chats/components/chatForm';
|
|
import { ChatMessage } from '@app/chats/components/message';
|
|
|
|
import { useArk } from '@libs/ark';
|
|
|
|
import { LoaderIcon } from '@shared/icons';
|
|
import { User } from '@shared/user';
|
|
|
|
import { useNostr } from '@utils/hooks/useNostr';
|
|
|
|
export function ChatScreen() {
|
|
const { ark } = useArk();
|
|
const { pubkey } = useParams();
|
|
const { fetchNIP04Messages } = useNostr();
|
|
const { status, data } = useQuery({
|
|
queryKey: ['nip04-dm', pubkey],
|
|
queryFn: async () => {
|
|
return await fetchNIP04Messages(pubkey);
|
|
},
|
|
refetchOnWindowFocus: false,
|
|
});
|
|
|
|
const queryClient = useQueryClient();
|
|
const listRef = useRef<VListHandle>(null);
|
|
|
|
const newMessage = useMutation({
|
|
mutationFn: async (event: NDKEvent) => {
|
|
// Cancel any outgoing refetches
|
|
await queryClient.cancelQueries({ queryKey: ['nip04-dm', pubkey] });
|
|
|
|
// Snapshot the previous value
|
|
const prevMessages = queryClient.getQueryData(['nip04-dm', pubkey]);
|
|
|
|
// Optimistically update to the new value
|
|
queryClient.setQueryData(['nip04-dm', pubkey], (prev: NDKEvent[]) => [
|
|
...prev,
|
|
event,
|
|
]);
|
|
|
|
// Return a context object with the snapshotted value
|
|
return { prevMessages };
|
|
},
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['nip04-dm', pubkey] });
|
|
},
|
|
});
|
|
|
|
const renderItem = useCallback(
|
|
(message: NDKEvent) => {
|
|
return (
|
|
<ChatMessage
|
|
key={message.id}
|
|
message={message}
|
|
isSelf={message.pubkey === ark.account.pubkey}
|
|
/>
|
|
);
|
|
},
|
|
[data]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (data && data.length > 0) listRef.current?.scrollToIndex(data.length);
|
|
}, [data]);
|
|
|
|
useEffect(() => {
|
|
const sub = ark.subscribe({
|
|
filter: {
|
|
kinds: [4],
|
|
authors: [ark.account.pubkey],
|
|
'#p': [pubkey],
|
|
since: Math.floor(Date.now() / 1000),
|
|
},
|
|
closeOnEose: false,
|
|
cb: (event) => newMessage.mutate(event),
|
|
});
|
|
|
|
return () => {
|
|
sub.stop();
|
|
};
|
|
}, [pubkey]);
|
|
|
|
return (
|
|
<div className="h-full w-full p-3">
|
|
<div className="rounded-lg bg-neutral-100 backdrop-blur-xl dark:bg-neutral-900">
|
|
<div className="flex h-full flex-col justify-between overflow-hidden">
|
|
<div className="flex h-16 shrink-0 items-center border-b border-neutral-200 px-3 dark:border-neutral-800">
|
|
<User pubkey={pubkey} variant="simple" />
|
|
</div>
|
|
<div className="h-full w-full flex-1 px-3 py-3">
|
|
{status === 'pending' ? (
|
|
<div className="flex h-full w-full items-center justify-center">
|
|
<div className="flex flex-col items-center gap-1.5">
|
|
<LoaderIcon className="h-5 w-5 animate-spin text-neutral-900 dark:text-neutral-100" />
|
|
<p className="text-sm font-medium text-neutral-500 dark:text-neutral-300">
|
|
Loading messages
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : data.length === 0 ? (
|
|
<div className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 transform flex-col gap-1 text-center">
|
|
<h3 className="mb-2 text-4xl">🙌</h3>
|
|
<p className="leading-none text-neutral-500 dark:text-neutral-300">
|
|
You two didn't talk yet, let's send first message
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<VList ref={listRef} className="h-full scrollbar-none" shift={true} reverse>
|
|
{data.map((message) => renderItem(message))}
|
|
</VList>
|
|
)}
|
|
</div>
|
|
<div className="shrink-0 rounded-b-lg border-t border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
|
|
<ChatForm receiverPubkey={pubkey} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|