mirror of
https://github.com/lumehq/lume.git
synced 2025-09-27 21:36:36 +02:00
added useDecryptMessage hook and updated chat messages
This commit is contained in:
@@ -4,17 +4,24 @@ import { MessageList } from '@components/chats/messageList';
|
|||||||
import FormChat from '@components/form/chat';
|
import FormChat from '@components/form/chat';
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { chatMessagesAtom } from '@stores/chat';
|
||||||
|
|
||||||
import useLocalStorage from '@rehooks/local-storage';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { useResetAtom } from 'jotai/utils';
|
||||||
|
import { Suspense, useCallback, useContext, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export default function Page({ params }: { params: { pubkey: string } }) {
|
export default function Page({ params }: { params: { pubkey: string } }) {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const [messages, setMessages] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const setChatMessages = useSetAtom(chatMessagesAtom);
|
||||||
const unsubscribe = pool.subscribe(
|
const resetChatMessages = useResetAtom(chatMessagesAtom);
|
||||||
|
|
||||||
|
const unsubscribe = useRef(null);
|
||||||
|
|
||||||
|
const fetchMessages = useCallback(() => {
|
||||||
|
unsubscribe.current = pool.subscribe(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
kinds: [4],
|
kinds: [4],
|
||||||
@@ -29,18 +36,29 @@ export default function Page({ params }: { params: { pubkey: string } }) {
|
|||||||
],
|
],
|
||||||
relays,
|
relays,
|
||||||
(event: any) => {
|
(event: any) => {
|
||||||
setMessages((messages) => [event, ...messages]);
|
setChatMessages((data) => [...data, event]);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}, [activeAccount.pubkey, params.pubkey, pool, relays, setChatMessages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// reset stored messages
|
||||||
|
resetChatMessages();
|
||||||
|
// fetch messages from relays
|
||||||
|
fetchMessages();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
if (unsubscribe.current) {
|
||||||
|
unsubscribe.current();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [pool, relays, params.pubkey, activeAccount.pubkey]);
|
}, [fetchMessages, resetChatMessages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-between">
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
<MessageList data={messages.sort((a, b) => a.created_at - b.created_at)} />
|
<Suspense fallback={<>Loading...</>}>
|
||||||
|
<MessageList />
|
||||||
|
</Suspense>
|
||||||
<div className="shrink-0 p-3">
|
<div className="shrink-0 p-3">
|
||||||
<FormChat receiverPubkey={params.pubkey} />
|
<FormChat receiverPubkey={params.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
|
@@ -23,9 +23,9 @@ export default function ChannelList() {
|
|||||||
<div className="flex flex-col gap-px">
|
<div className="flex flex-col gap-px">
|
||||||
<Link
|
<Link
|
||||||
href="/channels"
|
href="/channels"
|
||||||
className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-950"
|
className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900">
|
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800">
|
||||||
<Globe width={12} height={12} className="text-zinc-500" />
|
<Globe width={12} height={12} className="text-zinc-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@@ -51,8 +51,8 @@ export const CreateChannelModal = () => {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<div className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-950">
|
<div className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900">
|
||||||
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900">
|
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800">
|
||||||
<Plus width={12} height={12} className="text-zinc-500" />
|
<Plus width={12} height={12} className="text-zinc-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@@ -23,8 +23,8 @@ export const ChatModal = () => {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Root>
|
<Dialog.Root>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<div className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-950">
|
<div className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900">
|
||||||
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900">
|
<div className="group-hover:800 inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900">
|
||||||
<Plus width={12} height={12} className="text-zinc-500" />
|
<Plus width={12} height={12} className="text-zinc-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@@ -1,21 +1,23 @@
|
|||||||
import MessageListItem from '@components/chats/messageListItem';
|
import MessageListItem from '@components/chats/messageListItem';
|
||||||
|
import { Placeholder } from '@components/note/placeholder';
|
||||||
|
|
||||||
|
import { sortedChatMessagesAtom } from '@stores/chat';
|
||||||
|
|
||||||
import useLocalStorage from '@rehooks/local-storage';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
export const MessageList = ({ data }: { data: any }) => {
|
export const MessageList = () => {
|
||||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const virtuosoRef = useRef(null);
|
const virtuosoRef = useRef(null);
|
||||||
|
|
||||||
|
const data = useAtomValue(sortedChatMessagesAtom);
|
||||||
|
|
||||||
const itemContent: any = useCallback(
|
const itemContent: any = useCallback(
|
||||||
(index: string | number) => {
|
(index: string | number) => {
|
||||||
return (
|
return (
|
||||||
<MessageListItem
|
<MessageListItem data={data[index]} userPubkey={activeAccount.pubkey} userPrivkey={activeAccount.privkey} />
|
||||||
data={data[index]}
|
|
||||||
activeAccountPubkey={activeAccount.pubkey}
|
|
||||||
activeAccountPrivkey={activeAccount.privkey}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[activeAccount.privkey, activeAccount.pubkey, data]
|
[activeAccount.privkey, activeAccount.pubkey, data]
|
||||||
@@ -33,6 +35,7 @@ export const MessageList = ({ data }: { data: any }) => {
|
|||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={data}
|
data={data}
|
||||||
|
components={COMPONENTS}
|
||||||
itemContent={itemContent}
|
itemContent={itemContent}
|
||||||
computeItemKey={computeItemKey}
|
computeItemKey={computeItemKey}
|
||||||
initialTopMostItemIndex={data.length - 1}
|
initialTopMostItemIndex={data.length - 1}
|
||||||
@@ -45,3 +48,8 @@ export const MessageList = ({ data }: { data: any }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const COMPONENTS = {
|
||||||
|
EmptyPlaceholder: () => <Placeholder />,
|
||||||
|
ScrollSeekPlaceholder: () => <Placeholder />,
|
||||||
|
};
|
||||||
|
@@ -1,36 +1,11 @@
|
|||||||
import { MessageUser } from '@components/chats/messageUser';
|
import { MessageUser } from '@components/chats/messageUser';
|
||||||
|
|
||||||
import { nip04 } from 'nostr-tools';
|
import { useDecryptMessage } from '@utils/hooks/useDecryptMessage';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
const MessageListItem = ({
|
import { memo } from 'react';
|
||||||
data,
|
|
||||||
activeAccountPubkey,
|
|
||||||
activeAccountPrivkey,
|
|
||||||
}: {
|
|
||||||
data: any;
|
|
||||||
activeAccountPubkey: string;
|
|
||||||
activeAccountPrivkey: string;
|
|
||||||
}) => {
|
|
||||||
const [content, setContent] = useState('');
|
|
||||||
|
|
||||||
const sender = useMemo(() => {
|
const MessageListItem = ({ data, userPubkey, userPrivkey }: { data: any; userPubkey: string; userPrivkey: string }) => {
|
||||||
const pTag = data.tags.find(([k, v]) => k === 'p' && v && v !== '')[1];
|
const content = useDecryptMessage(userPubkey, userPrivkey, data.pubkey, data.tags, data.content);
|
||||||
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 (
|
return (
|
||||||
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-2 hover:bg-black/20">
|
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-2 hover:bg-black/20">
|
||||||
|
@@ -25,7 +25,6 @@ export default function EventCollector() {
|
|||||||
|
|
||||||
const now = useRef(new Date());
|
const now = useRef(new Date());
|
||||||
const unsubscribe = useRef(null);
|
const unsubscribe = useRef(null);
|
||||||
const unlisten = useRef(null);
|
|
||||||
|
|
||||||
const createFollowingPlebs = useCallback(
|
const createFollowingPlebs = useCallback(
|
||||||
async (tags) => {
|
async (tags) => {
|
||||||
@@ -115,7 +114,7 @@ export default function EventCollector() {
|
|||||||
}, [pool, relays, activeAccount.id, activeAccount.pubkey, follows, setHasNewerNote, createFollowingPlebs]);
|
}, [pool, relays, activeAccount.id, activeAccount.pubkey, follows, setHasNewerNote, createFollowingPlebs]);
|
||||||
|
|
||||||
const listenWindowClose = useCallback(async () => {
|
const listenWindowClose = useCallback(async () => {
|
||||||
unlisten.current = window.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
|
window.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
|
||||||
writeStorage('lastLogin', now.current);
|
writeStorage('lastLogin', now.current);
|
||||||
window.getCurrent().close();
|
window.getCurrent().close();
|
||||||
});
|
});
|
||||||
@@ -129,9 +128,6 @@ export default function EventCollector() {
|
|||||||
if (unsubscribe.current) {
|
if (unsubscribe.current) {
|
||||||
unsubscribe.current();
|
unsubscribe.current();
|
||||||
}
|
}
|
||||||
if (unlisten.current) {
|
|
||||||
unlisten.current;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [setHasNewerNote, subscribe, listenWindowClose]);
|
}, [setHasNewerNote, subscribe, listenWindowClose]);
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
import { ActiveLink } from '@components/activeLink';
|
import { ActiveLink } from '@components/activeLink';
|
||||||
|
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { NavArrowUp } from 'iconoir-react';
|
import { Bonfire, NavArrowUp, PeopleTag } from 'iconoir-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function Newsfeed() {
|
export default function Newsfeed() {
|
||||||
@@ -28,6 +28,7 @@ export default function Newsfeed() {
|
|||||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
||||||
>
|
>
|
||||||
|
<PeopleTag width={16} height={16} className="text-zinc-500" />
|
||||||
<span>Following</span>
|
<span>Following</span>
|
||||||
</ActiveLink>
|
</ActiveLink>
|
||||||
<ActiveLink
|
<ActiveLink
|
||||||
@@ -35,6 +36,7 @@ export default function Newsfeed() {
|
|||||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
||||||
>
|
>
|
||||||
|
<Bonfire width={16} height={16} className="text-zinc-500" />
|
||||||
<span>Circle</span>
|
<span>Circle</span>
|
||||||
</ActiveLink>
|
</ActiveLink>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
|
8
src/stores/chat.tsx
Normal file
8
src/stores/chat.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
import { atomWithReset } from 'jotai/utils';
|
||||||
|
|
||||||
|
export const chatMessagesAtom = atomWithReset([]);
|
||||||
|
export const sortedChatMessagesAtom = atom((get) => {
|
||||||
|
const messages = get(chatMessagesAtom);
|
||||||
|
return messages.sort((x: { created_at: number }, y: { created_at: number }) => x.created_at - y.created_at);
|
||||||
|
});
|
34
src/utils/hooks/useDecryptMessage.tsx
Normal file
34
src/utils/hooks/useDecryptMessage.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { nip04 } from 'nostr-tools';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const useDecryptMessage = (
|
||||||
|
userKey: string,
|
||||||
|
userPriv: string,
|
||||||
|
eventKey: string,
|
||||||
|
eventTags: string[],
|
||||||
|
encryptedContent: string
|
||||||
|
) => {
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
|
||||||
|
const extractSenderKey = useCallback(() => {
|
||||||
|
const keyInTags = eventTags.find(([k, v]) => k === 'p' && v && v !== '')[1];
|
||||||
|
if (keyInTags === userKey) {
|
||||||
|
return eventKey;
|
||||||
|
} else {
|
||||||
|
return keyInTags;
|
||||||
|
}
|
||||||
|
}, [eventKey, eventTags, userKey]);
|
||||||
|
|
||||||
|
const decrypt = useCallback(async () => {
|
||||||
|
const senderKey = extractSenderKey();
|
||||||
|
const result = await nip04.decrypt(userPriv, senderKey, encryptedContent);
|
||||||
|
// update state with decrypt content
|
||||||
|
setContent(result);
|
||||||
|
}, [userPriv, encryptedContent, extractSenderKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
decrypt().catch(console.error);
|
||||||
|
}, [decrypt]);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
@@ -16,14 +16,20 @@ export const useMetadata = (pubkey) => {
|
|||||||
const [profile, setProfile] = useState(null);
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
const cacheProfile = useMemo(() => {
|
const cacheProfile = useMemo(() => {
|
||||||
const findInStorage = plebs.find((item) => item.pubkey === pubkey);
|
let metadata = false;
|
||||||
|
|
||||||
if (findInStorage !== undefined) {
|
if (pubkey === activeAccount.pubkey) {
|
||||||
return JSON.parse(findInStorage.metadata);
|
metadata = JSON.parse(activeAccount.metadata);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
const findInStorage = plebs.find((item) => item.pubkey === pubkey);
|
||||||
|
|
||||||
|
if (findInStorage !== undefined) {
|
||||||
|
metadata = JSON.parse(findInStorage.metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [plebs, pubkey]);
|
|
||||||
|
return metadata;
|
||||||
|
}, [plebs, pubkey, activeAccount.pubkey, activeAccount.metadata]);
|
||||||
|
|
||||||
const insertPlebToDB = useCallback(
|
const insertPlebToDB = useCallback(
|
||||||
async (pubkey: string, metadata: string) => {
|
async (pubkey: string, metadata: string) => {
|
||||||
|
Reference in New Issue
Block a user