mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
make DM suck less
This commit is contained in:
parent
a0e814bfce
commit
c119e02a8a
5
.changeset/eleven-buckets-decide.md
Normal file
5
.changeset/eleven-buckets-decide.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add decrypt all button to DMs
|
5
.changeset/smart-monkeys-boil.md
Normal file
5
.changeset/smart-monkeys-boil.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Cache decrypted events
|
@ -233,8 +233,11 @@ const router = createHashRouter([
|
||||
{ path: "r/:relay", element: <RelayView /> },
|
||||
{ path: "notifications", element: <NotificationsView /> },
|
||||
{ path: "search", element: <SearchView /> },
|
||||
{ path: "dm", element: <DirectMessagesView /> },
|
||||
{ path: "dm/:key", element: <DirectMessageChatView /> },
|
||||
{
|
||||
path: "dm",
|
||||
element: <DirectMessagesView />,
|
||||
children: [{ path: ":pubkey", element: <DirectMessageChatView /> }],
|
||||
},
|
||||
{ path: "profile", element: <ProfileView /> },
|
||||
{
|
||||
path: "tools",
|
||||
|
142
src/providers/dycryption-provider.tsx
Normal file
142
src/providers/dycryption-provider.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useRef } from "react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import Subject from "../classes/subject";
|
||||
import { useSigningContext } from "./signing-provider";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import createDefer, { Deferred } from "../classes/deferred";
|
||||
|
||||
class DecryptionContainer {
|
||||
id = nanoid();
|
||||
pubkey: string;
|
||||
data: string;
|
||||
|
||||
plaintext = new Subject<string>();
|
||||
error = new Subject<Error>();
|
||||
|
||||
constructor(pubkey: string, data: string) {
|
||||
this.pubkey = pubkey;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
type DecryptionContextType = {
|
||||
getOrCreateContainer: (pubkey: string, data: string) => DecryptionContainer;
|
||||
startQueue: () => void;
|
||||
clearQueue: () => void;
|
||||
addToQueue: (container: DecryptionContainer) => Promise<string>;
|
||||
getQueue: () => DecryptionContainer[];
|
||||
};
|
||||
const DecryptionContext = createContext<DecryptionContextType>({
|
||||
getOrCreateContainer: () => {
|
||||
throw new Error("No DecryptionProvider");
|
||||
},
|
||||
startQueue: () => {},
|
||||
clearQueue: () => {},
|
||||
addToQueue: () => Promise.reject(new Error("No DecryptionProvider")),
|
||||
getQueue: () => [],
|
||||
});
|
||||
|
||||
export function useDecryptionContext(){
|
||||
return useContext(DecryptionContext)
|
||||
}
|
||||
export function useDecryptionContainer(pubkey: string, data: string) {
|
||||
const { getOrCreateContainer, addToQueue, startQueue } = useContext(DecryptionContext);
|
||||
const container = getOrCreateContainer(pubkey, data);
|
||||
|
||||
const plaintext = useSubject(container.plaintext);
|
||||
const error = useSubject(container.error);
|
||||
|
||||
const requestDecrypt = useCallback(() => {
|
||||
const p = addToQueue(container);
|
||||
startQueue();
|
||||
return p;
|
||||
}, [addToQueue, startQueue]);
|
||||
|
||||
return { container, error, plaintext, requestDecrypt };
|
||||
}
|
||||
|
||||
export default function DecryptionProvider({ children }: PropsWithChildren) {
|
||||
const { requestDecrypt } = useSigningContext();
|
||||
|
||||
const containers = useRef<DecryptionContainer[]>([]);
|
||||
const queue = useRef<DecryptionContainer[]>([]);
|
||||
const promises = useRef<Map<DecryptionContainer, Deferred<string>>>(new Map());
|
||||
const running = useRef<boolean>(false);
|
||||
|
||||
const getQueue = useCallback(() => queue.current, []);
|
||||
const clearQueue = useCallback(() => {
|
||||
queue.current = [];
|
||||
promises.current.clear();
|
||||
}, []);
|
||||
const addToQueue = useCallback((container: DecryptionContainer) => {
|
||||
queue.current.unshift(container);
|
||||
let p = promises.current.get(container);
|
||||
if (!p) {
|
||||
p = createDefer<string>();
|
||||
promises.current.set(container, p);
|
||||
}
|
||||
return p;
|
||||
}, []);
|
||||
|
||||
const getOrCreateContainer = useCallback((pubkey: string, data: string) => {
|
||||
let container = containers.current.find((c) => c.pubkey === pubkey && c.data === data);
|
||||
if (!container) {
|
||||
container = new DecryptionContainer(pubkey, data);
|
||||
containers.current.push(container);
|
||||
}
|
||||
return container;
|
||||
}, []);
|
||||
|
||||
const startQueue = useCallback(() => {
|
||||
if (running.current === true) return;
|
||||
running.current = false;
|
||||
|
||||
async function decryptNext() {
|
||||
if (running.current === true) return;
|
||||
|
||||
const container = queue.current.pop();
|
||||
if (!container) {
|
||||
running.current = false;
|
||||
promises.current.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = promises.current.get(container)!;
|
||||
|
||||
try {
|
||||
const plaintext = await requestDecrypt(container.data, container.pubkey);
|
||||
|
||||
// set plaintext
|
||||
container.plaintext.next(plaintext);
|
||||
promise.resolve(plaintext);
|
||||
|
||||
// remove promise
|
||||
promises.current.delete(container);
|
||||
|
||||
setTimeout(() => decryptNext(), 100);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
// set error
|
||||
container.error.next(e);
|
||||
promise.reject(e);
|
||||
|
||||
// clear queue
|
||||
running.current = false;
|
||||
queue.current = [];
|
||||
promises.current.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// start cycle
|
||||
decryptNext();
|
||||
}, [requestDecrypt]);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({ getQueue, addToQueue, clearQueue, getOrCreateContainer, startQueue }),
|
||||
[getQueue, addToQueue, clearQueue, getOrCreateContainer, startQueue],
|
||||
);
|
||||
|
||||
return <DecryptionContext.Provider value={context}>{children}</DecryptionContext.Provider>;
|
||||
}
|
@ -12,6 +12,7 @@ import { DefaultEmojiProvider, UserEmojiProvider } from "./emoji-provider";
|
||||
import { AllUserSearchDirectoryProvider } from "./user-directory-provider";
|
||||
import MuteModalProvider from "./mute-modal-provider";
|
||||
import BreakpointProvider from "./breakpoint-provider";
|
||||
import DecryptionProvider from "./dycryption-provider";
|
||||
|
||||
// Top level providers, should be render as close to the root as possible
|
||||
export const GlobalProviders = ({ children }: { children: React.ReactNode }) => {
|
||||
@ -33,21 +34,23 @@ export function PageProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<BreakpointProvider>
|
||||
<SigningProvider>
|
||||
<DeleteEventProvider>
|
||||
<MuteModalProvider>
|
||||
<InvoiceModalProvider>
|
||||
<NotificationTimelineProvider>
|
||||
<DefaultEmojiProvider>
|
||||
<UserEmojiProvider>
|
||||
<AllUserSearchDirectoryProvider>
|
||||
<PostModalProvider>{children}</PostModalProvider>
|
||||
</AllUserSearchDirectoryProvider>
|
||||
</UserEmojiProvider>
|
||||
</DefaultEmojiProvider>
|
||||
</NotificationTimelineProvider>
|
||||
</InvoiceModalProvider>
|
||||
</MuteModalProvider>
|
||||
</DeleteEventProvider>
|
||||
<DecryptionProvider>
|
||||
<DeleteEventProvider>
|
||||
<MuteModalProvider>
|
||||
<InvoiceModalProvider>
|
||||
<NotificationTimelineProvider>
|
||||
<DefaultEmojiProvider>
|
||||
<UserEmojiProvider>
|
||||
<AllUserSearchDirectoryProvider>
|
||||
<PostModalProvider>{children}</PostModalProvider>
|
||||
</AllUserSearchDirectoryProvider>
|
||||
</UserEmojiProvider>
|
||||
</DefaultEmojiProvider>
|
||||
</NotificationTimelineProvider>
|
||||
</InvoiceModalProvider>
|
||||
</MuteModalProvider>
|
||||
</DeleteEventProvider>
|
||||
</DecryptionProvider>
|
||||
</SigningProvider>
|
||||
</BreakpointProvider>
|
||||
);
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Card, CardBody, Flex, IconButton, Textarea, useToast } from "@chakra-ui/react";
|
||||
import { Button, Card, Flex, IconButton, Textarea, useToast } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { Navigate, useNavigate, useParams } from "react-router-dom";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { ChevronLeftIcon } from "../../components/icons";
|
||||
import UserAvatar from "../../components/user-avatar";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { normalizeToHex } from "../../helpers/nip19";
|
||||
import { isHexKey } from "../../helpers/nip19";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
@ -22,17 +22,26 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import { LightboxProvider } from "../../components/lightbox-provider";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import { useDecryptionContext } from "../../providers/dycryption-provider";
|
||||
import { useUserRelays } from "../../hooks/use-user-relays";
|
||||
import { RelayMode } from "../../classes/relay";
|
||||
import { unique } from "../../helpers/array";
|
||||
|
||||
function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const account = useCurrentAccount()!;
|
||||
const { getOrCreateContainer, addToQueue, startQueue } = useDecryptionContext();
|
||||
const { requestEncrypt, requestSignature } = useSigningContext();
|
||||
const [content, setContent] = useState<string>("");
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const myInbox = useReadRelayUrls();
|
||||
const usersInbox = useUserRelays(pubkey)
|
||||
.filter((r) => r.mode & RelayMode.READ)
|
||||
.map((r) => r.url);
|
||||
|
||||
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, readRelays, [
|
||||
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, myInbox, [
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
"#p": [account.pubkey],
|
||||
@ -59,48 +68,88 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
};
|
||||
const signed = await requestSignature(event);
|
||||
const writeRelays = clientRelaysService.getWriteUrls();
|
||||
const pub = new NostrPublishAction("Send DM", writeRelays, signed);
|
||||
const relays = unique([...writeRelays, ...usersInbox]);
|
||||
new NostrPublishAction("Send DM", relays, signed);
|
||||
setContent("");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
};
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const decryptAll = async () => {
|
||||
const promises = messages
|
||||
.map((message) => {
|
||||
const container = getOrCreateContainer(pubkey, message.content);
|
||||
if (container.plaintext.value === undefined) return addToQueue(container);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
startQueue();
|
||||
|
||||
setLoading(true);
|
||||
Promise.all(promises).finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<LightboxProvider>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex maxH={{ base: "calc(100vh - 3.5rem)", md: "100vh" }} overflow="hidden" direction="column">
|
||||
<Card size="sm" flexShrink={0}>
|
||||
<CardBody display="flex" gap="2" alignItems="center">
|
||||
<IconButton variant="ghost" icon={<ChevronLeftIcon />} aria-label="Back" onClick={() => navigate(-1)} />
|
||||
<UserAvatar pubkey={pubkey} size="sm" />
|
||||
<UserLink pubkey={pubkey} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column-reverse" gap="2" py="4" px="2">
|
||||
{[...messages].map((event) => (
|
||||
<Message key={event.id} event={event} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</Flex>
|
||||
<Flex shrink={0}>
|
||||
<Textarea value={content} onChange={(e) => setContent(e.target.value)} />
|
||||
<Button isDisabled={!content} onClick={sendMessage}>
|
||||
Send
|
||||
</Button>
|
||||
<Card size="sm" flexShrink={0} p="2" flexDirection="row">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
icon={<ChevronLeftIcon />}
|
||||
aria-label="Back"
|
||||
onClick={() => navigate(-1)}
|
||||
hideFrom="xl"
|
||||
/>
|
||||
<UserAvatar pubkey={pubkey} size="sm" />
|
||||
<UserLink pubkey={pubkey} fontWeight="bold" />
|
||||
<UserDnsIdentityIcon pubkey={pubkey} onlyIcon />
|
||||
</Flex>
|
||||
<Button onClick={decryptAll} isLoading={loading} ml="auto">
|
||||
Decrypt All
|
||||
</Button>
|
||||
</Card>
|
||||
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column-reverse" gap="2" py="4" px="2">
|
||||
{[...messages].map((event) => (
|
||||
<Message key={event.id} event={event} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</Flex>
|
||||
<Flex shrink={0}>
|
||||
<Textarea value={content} onChange={(e) => setContent(e.target.value)} />
|
||||
<Button isDisabled={!content} onClick={sendMessage}>
|
||||
Send
|
||||
</Button>
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
</LightboxProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function useUserPointer() {
|
||||
const { pubkey } = useParams() as { pubkey: string };
|
||||
|
||||
if (isHexKey(pubkey)) return { pubkey, relays: [] };
|
||||
const pointer = nip19.decode(pubkey);
|
||||
|
||||
switch (pointer.type) {
|
||||
case "npub":
|
||||
return { pubkey: pointer.data as string, relays: [] };
|
||||
case "nprofile":
|
||||
const d = pointer.data as nip19.ProfilePointer;
|
||||
return { pubkey: d.pubkey, relays: d.relays ?? [] };
|
||||
default:
|
||||
throw new Error(`Unknown type ${pointer.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function DirectMessageChatView() {
|
||||
const { key } = useParams();
|
||||
if (!key) return <Navigate to="/" />;
|
||||
const pubkey = normalizeToHex(key);
|
||||
if (!pubkey) throw new Error("invalid pubkey");
|
||||
const { pubkey } = useUserPointer();
|
||||
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<DirectMessageChatPage pubkey={pubkey} />
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Alert, AlertDescription, AlertIcon, Button } from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { Alert, AlertDescription, AlertIcon, Button } from "@chakra-ui/react";
|
||||
|
||||
import { UnlockIcon } from "../../components/icons";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import { useDecryptionContainer } from "../../providers/dycryption-provider";
|
||||
|
||||
export default function DecryptPlaceholder({
|
||||
children,
|
||||
@ -13,30 +13,28 @@ export default function DecryptPlaceholder({
|
||||
data: string;
|
||||
pubkey: string;
|
||||
}): JSX.Element {
|
||||
const { requestDecrypt } = useSigningContext();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [decrypted, setDecrypted] = useState<string>();
|
||||
const [error, setError] = useState<Error>();
|
||||
const { requestDecrypt, plaintext, error } = useDecryptionContainer(pubkey, data);
|
||||
|
||||
const decrypt = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const decrypted = await requestDecrypt(data, pubkey);
|
||||
if (decrypted) setDecrypted(decrypted);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) setError(e);
|
||||
}
|
||||
await requestDecrypt();
|
||||
} catch (e) {}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (decrypted) {
|
||||
return children(decrypted);
|
||||
if (plaintext) {
|
||||
return children(plaintext);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
<Button isLoading={loading} leftIcon={<UnlockIcon />} onClick={decrypt} size="sm" ml="auto">
|
||||
Try again
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
@ -9,13 +9,16 @@ import {
|
||||
Card,
|
||||
CardBody,
|
||||
Flex,
|
||||
Input,
|
||||
Link,
|
||||
LinkBox,
|
||||
LinkOverlay,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Outlet, Link as RouterLink, useLocation, useParams } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import UserAvatar from "../../components/user-avatar";
|
||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
@ -23,14 +26,16 @@ import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import directMessagesService from "../../services/direct-messages";
|
||||
import { ExternalLinkIcon } from "../../components/icons";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
|
||||
function ContactCard({ pubkey }: { pubkey: string }) {
|
||||
const subject = useMemo(() => directMessagesService.getUserMessages(pubkey), [pubkey]);
|
||||
const messages = useSubject(subject);
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<LinkBox as={Card} size="sm">
|
||||
@ -41,12 +46,14 @@ function ContactCard({ pubkey }: { pubkey: string }) {
|
||||
{messages[0] && <Timestamp flexShrink={0} timestamp={messages[0].created_at} />}
|
||||
</Flex>
|
||||
</CardBody>
|
||||
<LinkOverlay as={RouterLink} to={`/dm/${nip19.npubEncode(pubkey)}`} />
|
||||
<LinkOverlay as={RouterLink} to={`/dm/${nip19.npubEncode(pubkey)}` + location.search} />
|
||||
</LinkBox>
|
||||
);
|
||||
}
|
||||
|
||||
function DirectMessagesPage() {
|
||||
const params = useParams();
|
||||
const { people } = usePeopleListContext();
|
||||
const [from, setFrom] = useState(dayjs().subtract(2, "days").unix());
|
||||
const conversations = useSubject(directMessagesService.conversations);
|
||||
|
||||
@ -62,67 +69,54 @@ function DirectMessagesPage() {
|
||||
};
|
||||
|
||||
const sortedConversations = useMemo(() => {
|
||||
return Array.from(conversations).sort((a, b) => {
|
||||
const latestA = directMessagesService.getUserMessages(a).value[0]?.created_at ?? 0;
|
||||
const latestB = directMessagesService.getUserMessages(b).value[0]?.created_at ?? 0;
|
||||
return Array.from(conversations)
|
||||
.filter((pubkey) => (people ? people.some((p) => p.pubkey === pubkey) : true))
|
||||
.sort((a, b) => {
|
||||
const latestA = directMessagesService.getUserMessages(a).value[0]?.created_at ?? 0;
|
||||
const latestB = directMessagesService.getUserMessages(b).value[0]?.created_at ?? 0;
|
||||
|
||||
return latestB - latestA;
|
||||
});
|
||||
}, [conversations]);
|
||||
return latestB - latestA;
|
||||
});
|
||||
}, [conversations, people]);
|
||||
|
||||
const isChatOpen = !!params.pubkey;
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Alert status="info" flexShrink={0}>
|
||||
<AlertIcon />
|
||||
<Flex direction={{ base: "column", lg: "row" }}>
|
||||
<AlertTitle>Give Blowater a try</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Text>
|
||||
Its a much better chat app than what I can build inside of noStrudel.{" "}
|
||||
<Link href="https://blowater.app/" isExternal>
|
||||
blowater.app <ExternalLinkIcon />
|
||||
</Link>
|
||||
</Text>
|
||||
</AlertDescription>
|
||||
<Flex gap="4" maxH={{ base: "calc(100vh - 3.5rem)", md: "100vh" }}>
|
||||
<Flex
|
||||
gap="2"
|
||||
direction="column"
|
||||
w={!isChatOpen ? { base: "full", lg: "sm" } : "sm"}
|
||||
overflowX="hidden"
|
||||
overflowY="auto"
|
||||
py="2"
|
||||
px={{ base: "2", lg: 0 }}
|
||||
hideBelow={!isChatOpen ? undefined : "xl"}
|
||||
>
|
||||
<Flex gap="2">
|
||||
{/* <Input type="search" placeholder="Search" /> */}
|
||||
<PeopleListSelection flexShrink={0} />
|
||||
</Flex>
|
||||
</Alert>
|
||||
|
||||
{conversations.length === 0 ? (
|
||||
<Alert
|
||||
status="info"
|
||||
variant="subtle"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
textAlign="center"
|
||||
height="200px"
|
||||
>
|
||||
<AlertIcon boxSize="40px" mr={0} />
|
||||
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||
No direct messages yet :(
|
||||
</AlertTitle>
|
||||
<AlertDescription maxWidth="sm">
|
||||
Click <ChatIcon /> on another users profile to start a conversation.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
{sortedConversations.map((pubkey) => (
|
||||
<ContactCard key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
<Button onClick={loadMore} isLoading={loading} flexShrink={0}>
|
||||
Load More
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</VerticalPageLayout>
|
||||
{sortedConversations.map((pubkey) => (
|
||||
<ContactCard key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
<Button onClick={loadMore} isLoading={loading} flexShrink={0}>
|
||||
Load More
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex gap="2" direction="column" flex={1} hideBelow={!isChatOpen ? "xl" : undefined}>
|
||||
<Outlet />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DirectMessagesView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<DirectMessagesPage />
|
||||
<PeopleListProvider initList="global">
|
||||
<DirectMessagesPage />
|
||||
</PeopleListProvider>
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user