re-enable nip-04 with more polish, prepare for nip-44

This commit is contained in:
Ren Amamiya
2023-08-24 09:05:34 +07:00
parent 3455eb701f
commit 4893ebd932
19 changed files with 376 additions and 242 deletions

View File

@@ -5,10 +5,9 @@ import { Image } from '@shared/image';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
import { Chats } from '@utils/types';
export function ChatsListItem({ data }: { data: Chats }) {
const { status, user } = useProfile(data.sender_pubkey);
export function ChatsListItem({ pubkey }: { pubkey: string }) {
const { status, user } = useProfile(pubkey);
if (status === 'loading') {
return (
@@ -21,7 +20,7 @@ export function ChatsListItem({ data }: { data: Chats }) {
return (
<NavLink
to={`/chats/${data.sender_pubkey}`}
to={`/chats/${pubkey}`}
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
@@ -32,23 +31,13 @@ export function ChatsListItem({ data }: { data: Chats }) {
>
<Image
src={user?.picture || user?.image}
alt={data.sender_pubkey}
alt={pubkey}
className="h-6 w-6 shrink-0 rounded object-cover"
/>
<div className="inline-flex w-full flex-1 items-center justify-between">
<h5 className="max-w-[10rem] truncate">
{user?.nip05 ||
user?.name ||
user?.display_name ||
displayNpub(data.sender_pubkey, 16)}
{user?.nip05 || user?.name || user?.display_name || displayNpub(pubkey, 16)}
</h5>
<div className="flex items-center">
{data.new_messages > 0 && (
<span className="inline-flex w-8 items-center justify-center rounded bg-fuchsia-400/10 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">
{data.new_messages}
</span>
)}
</div>
</div>
</NavLink>
);

View File

@@ -8,18 +8,23 @@ import { UnknownsModal } from '@app/chats/components/unknowns';
import { useStorage } from '@libs/storage/provider';
import { Chats } from '@utils/types';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatsList() {
const { db } = useStorage();
const { status, data: chats } = useQuery(['chats'], async () => {
return { follows: [], unknowns: [] };
});
const { fetchNIP04Chats } = useNostr();
const { status, data: chats } = useQuery(
['nip04-chats'],
async () => {
return await fetchNIP04Chats();
},
{ refetchOnWindowFocus: false }
);
const renderItem = useCallback(
(item: Chats) => {
if (db.account.pubkey !== item.sender_pubkey) {
return <ChatsListItem key={item.sender_pubkey} data={item} />;
(item: string) => {
if (db.account.pubkey !== item) {
return <ChatsListItem key={item} pubkey={item} />;
}
},
[chats]

View File

@@ -1,8 +1,9 @@
import { nip04 } from 'nostr-tools';
import { useCallback, useState } from 'react';
import { MediaUploader } from '@app/chats/components/messages/mediaUploader';
import { EnterIcon } from '@shared/icons';
import { MediaUploader } from '@shared/mediaUploader';
import { useNostr } from '@utils/hooks/useNostr';
@@ -34,7 +35,7 @@ export function ChatMessageForm({
const handleEnterPress = (e: {
key: string;
shiftKey: any;
shiftKey: KeyboardEvent['shiftKey'];
preventDefault: () => void;
}) => {
if (e.key === 'Enter' && !e.shiftKey) {

View File

@@ -1,32 +1,31 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage';
import { NoteContent } from '@shared/notes';
import { User } from '@shared/user';
import { parser } from '@utils/parser';
export function ChatMessageItem({
data,
message,
userPubkey,
userPrivkey,
}: {
data: any;
message: NDKEvent;
userPubkey: string;
userPrivkey: string;
}) {
const decryptedContent = useDecryptMessage(data, userPubkey, userPrivkey);
const decryptedContent = useDecryptMessage(message, userPubkey, userPrivkey);
// if we have decrypted content, use it instead of the encrypted content
if (decryptedContent) {
data['content'] = decryptedContent;
message['content'] = decryptedContent;
}
return (
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-white/10">
<div className="flex flex-col">
<User pubkey={data.sender_pubkey} time={data.created_at} isChat={true} />
<User pubkey={message.pubkey} time={message.created_at} isChat={true} />
<div className="-mt-[20px] pl-[49px]">
<p className="select-text whitespace-pre-line break-words text-base text-white">
{data.content}
{message.content}
</p>
</div>
</div>

View File

@@ -1,22 +1,25 @@
import * as Tooltip from '@radix-ui/react-tooltip';
import { useState } from 'react';
import { Dispatch, SetStateAction, useState } from 'react';
import { LoaderIcon, MediaIcon } from '@shared/icons';
import { useImageUploader } from '@utils/hooks/useUploader';
export function MediaUploader({ setState }: { setState: any }) {
export function MediaUploader({
setState,
}: {
setState: Dispatch<SetStateAction<string>>;
}) {
const upload = useImageUploader();
const [loading, setLoading] = useState(false);
const uploadMedia = async () => {
setLoading(true);
const image = await upload(null);
if (image.url) {
// update state
setState((prev: string) => `${prev}\n${image.url}`);
// stop loading
setLoading(false);
}
setLoading(false);
};
return (

View File

@@ -7,9 +7,8 @@ import { User } from '@app/auth/components/user';
import { CancelIcon, PlusIcon } from '@shared/icons';
import { compactNumber } from '@utils/number';
import { Chats } from '@utils/types';
export function UnknownsModal({ data }: { data: Chats[] }) {
export function UnknownsModal({ data }: { data: string[] }) {
const navigate = useNavigate();
const [open, setOpen] = useState(false);
@@ -55,16 +54,16 @@ export function UnknownsModal({ data }: { data: Chats[] }) {
</div>
</div>
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-2 pt-2">
{data.map((user) => (
{data.map((pubkey) => (
<div
key={user.sender_pubkey}
key={pubkey}
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
>
<User pubkey={user.sender_pubkey} />
<User pubkey={pubkey} />
<div>
<button
type="button"
onClick={() => openChat(user.sender_pubkey)}
onClick={() => openChat(pubkey)}
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
>
Chat

View File

@@ -1,16 +1,21 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { nip04 } from 'nostr-tools';
import { useEffect, useState } from 'react';
import { Chats } from '@utils/types';
export function useDecryptMessage(data: Chats, userPubkey: string, userPriv: string) {
const [content, setContent] = useState(data.content);
export function useDecryptMessage(
message: NDKEvent,
userPubkey: string,
userPriv: string
) {
const [content, setContent] = useState(message.content);
useEffect(() => {
async function decrypt() {
const pubkey =
userPubkey === data.sender_pubkey ? data.receiver_pubkey : data.sender_pubkey;
const result = await nip04.decrypt(userPriv, pubkey, data.content);
userPubkey === message.pubkey
? message.tags.find((el) => el[0] === 'p')[1]
: message.pubkey;
const result = await nip04.decrypt(userPriv, pubkey, message.content);
setContent(result);
}

View File

@@ -13,23 +13,27 @@ import { useStorage } from '@libs/storage/provider';
import { useStronghold } from '@stores/stronghold';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatScreen() {
const virtuosoRef = useRef(null);
const { ndk } = useNDK();
const { db } = useStorage();
const { pubkey } = useParams();
const { status, data } = useQuery(['chat', pubkey], async () => {
return [];
});
const userPrivkey = useStronghold((state) => state.privkey);
const { db } = useStorage();
const { ndk } = useNDK();
const { pubkey } = useParams();
const { fetchNIP04Messages } = useNostr();
const { status, data } = useQuery(['nip04-dm', pubkey], async () => {
return await fetchNIP04Messages(pubkey);
});
const itemContent = useCallback(
(index: string | number) => {
const message = data[index];
if (!message) return;
return (
<ChatMessageItem
data={data[index]}
message={message}
userPubkey={db.account.pubkey}
userPrivkey={userPrivkey}
/>

View File

@@ -131,8 +131,6 @@ export function NetworkWidget() {
};
sub(filter, async (event) => {
console.log('[network] new event', event.id);
let root: string;
let reply: string;
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {

View File

@@ -1,10 +1,16 @@
import { useState } from 'react';
import { Dispatch, SetStateAction, useState } from 'react';
import { LoaderIcon, PlusIcon } from '@shared/icons';
import { useImageUploader } from '@utils/hooks/useUploader';
export function AvatarUploader({ setPicture }: { setPicture: any }) {
export function AvatarUploader({
setPicture,
}: {
setPicture: Dispatch<
SetStateAction<{ url: undefined | string; error?: undefined | string }>
>;
}) {
const upload = useImageUploader();
const [loading, setLoading] = useState(false);

View File

@@ -1,10 +1,16 @@
import { useState } from 'react';
import { Dispatch, SetStateAction, useState } from 'react';
import { LoaderIcon, PlusIcon } from '@shared/icons';
import { useImageUploader } from '@utils/hooks/useUploader';
export function BannerUploader({ setBanner }: { setBanner: any }) {
export function BannerUploader({
setBanner,
}: {
setBanner: Dispatch<
SetStateAction<{ url: undefined | string; error?: undefined | string }>
>;
}) {
const upload = useImageUploader();
const [loading, setLoading] = useState(false);

View File

@@ -1,9 +1,9 @@
import * as Collapsible from '@radix-ui/react-collapsible';
import { useState } from 'react';
import { NavLink, useNavigate } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
// import { ChatsList } from '@app/chats/components/list';
import { ChatsList } from '@app/chats/components/list';
import { ComposerModal } from '@shared/composer/modal';
import {
ArrowLeftIcon,
@@ -14,11 +14,13 @@ import {
} from '@shared/icons';
import { LumeBar } from '@shared/lumeBar';
import { useSidebar } from '@stores/sidebar';
export function Navigation() {
const navigate = useNavigate();
const [feeds, setFeeds] = useState(true);
// const [chats, setChats] = useState(true);
const [feeds, toggleFeeds] = useSidebar((state) => [state.feeds, state.toggleFeeds]);
const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]);
return (
<div className="relative h-full w-[232px] bg-black/80">
@@ -43,7 +45,7 @@ export function Navigation() {
</button>
</div>
</div>
<Collapsible.Root open={feeds} onOpenChange={setFeeds}>
<Collapsible.Root open={feeds} onOpenChange={toggleFeeds}>
<div className="flex flex-col gap-1 px-2">
<Collapsible.Trigger asChild>
<button className="flex items-center gap-1">
@@ -80,7 +82,7 @@ export function Navigation() {
<span className="inline-flex h-6 w-6 items-center justify-center rounded bg-white/10">
<SpaceIcon className="h-3 w-3 text-white" />
</span>
<span className="text-sm font-medium">Space</span>
Space
</NavLink>
<NavLink
to="/notifications"
@@ -95,12 +97,34 @@ export function Navigation() {
<span className="inline-flex h-6 w-6 items-center justify-center rounded bg-white/10">
<BellIcon className="h-3 w-3 text-white" />
</span>
<span className="text-sm font-medium">Notifications</span>
Notifications
</NavLink>
</div>
</Collapsible.Content>
</div>
</Collapsible.Root>
<Collapsible.Root open={chats} onOpenChange={toggleChats}>
<div className="flex flex-col gap-1 px-2">
<Collapsible.Trigger asChild>
<button className="flex items-center gap-1">
<div
className={twMerge(
'inline-flex h-5 w-5 transform items-center justify-center transition-transform duration-150 ease-in-out',
open ? '' : 'rotate-180'
)}
>
<NavArrowDownIcon className="h-3 w-3 text-white/50" />
</div>
<h3 className="text-[11px] font-bold uppercase tracking-widest text-white/50">
Chats
</h3>
</button>
</Collapsible.Trigger>
<Collapsible.Content>
<ChatsList />
</Collapsible.Content>
</div>
</Collapsible.Root>
</div>
<LumeBar />
</div>

View File

@@ -25,6 +25,7 @@ export function TextNote({ event }: { event: NDKEvent }) {
components={{
del: ({ children }) => {
const key = children[0] as string;
if (!key) return;
if (key.startsWith('pub') && key.length > 50 && key.length < 100)
return <MentionUser pubkey={key.replace('pub-', '')} />;
if (key.startsWith('tag')) return <Hashtag tag={key.replace('tag-', '')} />;

24
src/stores/sidebar.ts Normal file
View File

@@ -0,0 +1,24 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
interface SidebarState {
feeds: boolean;
chats: boolean;
toggleFeeds: () => void;
toggleChats: () => void;
}
export const useSidebar = create<SidebarState>()(
persist(
(set) => ({
feeds: true,
chats: true,
toggleFeeds: () => set((state) => ({ feeds: !state.feeds })),
toggleChats: () => set((state) => ({ chats: !state.chats })),
}),
{
name: 'sidebar',
storage: createJSONStorage(() => localStorage),
}
)
);

View File

@@ -166,6 +166,54 @@ export function useNostr() {
}
};
const fetchNIP04Chats = async () => {
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
const events = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
'#p': [db.account.pubkey],
},
{ since: 0 }
);
const senders = events.map((e) => e.pubkey);
const follows = new Set(senders.filter((el) => db.account.follows.includes(el)));
const unknowns = new Set(senders.filter((el) => !db.account.follows.includes(el)));
return { follows: [...follows], unknowns: [...unknowns] };
};
const fetchNIP04Messages = async (sender: string) => {
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
const senderMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [sender],
'#p': [db.account.pubkey],
},
{ since: 0 }
);
const userMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [db.account.pubkey],
'#p': [sender],
},
{ since: 0 }
);
const all = [...senderMessages, ...userMessages].sort(
(a, b) => a.created_at - b.created_at
);
return all as unknown as NDKEvent[];
};
const publish = async ({
content,
kind,
@@ -207,5 +255,14 @@ export function useNostr() {
return res;
};
return { sub, fetchUserData, prefetchEvents, fetchActivities, publish, createZap };
return {
sub,
fetchUserData,
prefetchEvents,
fetchActivities,
fetchNIP04Chats,
fetchNIP04Messages,
publish,
createZap,
};
}

View File

@@ -65,10 +65,12 @@ export function useImageUploader() {
return {
url: url,
error: null,
};
}
return {
url: null,
error: 'Upload failed',
};
};