mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
group DMs
This commit is contained in:
parent
193b7b6a17
commit
6d0fd9e960
@ -15,7 +15,7 @@ import { RelayMode } from "../../classes/relay";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import Message from "../../views/dms/message";
|
||||
import MessageBlock from "../../views/dms/message-block";
|
||||
import { LightboxProvider } from "../lightbox-provider";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
@ -99,7 +99,7 @@ export default function ChatWindow({ pubkey, onClose }: { pubkey: string; onClos
|
||||
<LightboxProvider>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{messages.map((event) => (
|
||||
<Message key={event.id} event={event} />
|
||||
<MessageBlock key={event.id} events={event} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
</LightboxProvider>
|
||||
|
@ -6,7 +6,7 @@ import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import Timestamp from "../../timestamp";
|
||||
import DecryptPlaceholder from "../../../views/dms/decrypt-placeholder";
|
||||
import { MessageContent } from "../../../views/dms/message";
|
||||
import { MessageContent } from "../../../views/dms/message-block";
|
||||
import { getMessageRecipient } from "../../../services/direct-messages";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
|
||||
|
@ -9,7 +9,7 @@ import UserLink from "../../components/user-link";
|
||||
import { isHexKey } from "../../helpers/nip19";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import Message from "./message";
|
||||
import MessageBlock from "./message-block";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
@ -20,7 +20,10 @@ import { LightboxProvider } from "../../components/lightbox-provider";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import { useDecryptionContext } from "../../providers/dycryption-provider";
|
||||
import SendMessageForm from "./send-message-form";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const GROUP_MESSAGES_LESS_THAN_MIN = 5;
|
||||
function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
const navigate = useNavigate();
|
||||
const account = useCurrentAccount()!;
|
||||
@ -41,7 +44,24 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
},
|
||||
]);
|
||||
|
||||
const messages = useSubject(timeline.timeline);
|
||||
const messages = useSubject(timeline.timeline).filter((e) => !e.tags.some((t) => t[0] === "e" && t[3] === "root"));
|
||||
|
||||
const grouped: { id: string; events: NostrEvent[] }[] = [];
|
||||
for (const message of messages) {
|
||||
const last = grouped[grouped.length - 1];
|
||||
if (last && last.events[0]?.pubkey === message.pubkey) {
|
||||
const lastEvent = last.events[last.events.length - 1];
|
||||
if (
|
||||
lastEvent &&
|
||||
dayjs.unix(lastEvent.created_at).diff(dayjs.unix(message.created_at), "minute") < GROUP_MESSAGES_LESS_THAN_MIN
|
||||
) {
|
||||
last.events.push(message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
grouped.push({ id: message.id, events: [message] });
|
||||
}
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const decryptAll = async () => {
|
||||
@ -81,8 +101,8 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
</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} />
|
||||
{grouped.map((group) => (
|
||||
<MessageBlock key={group.id} events={group.events} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</Flex>
|
||||
|
@ -82,7 +82,7 @@ function DirectMessagesPage() {
|
||||
const isChatOpen = !!params.pubkey;
|
||||
|
||||
return (
|
||||
<Flex gap="4" h={{ base: "calc(100vh - 3.5rem)", md: "100vh" }}>
|
||||
<Flex gap="4" h={{ base: "calc(100vh - 3.5rem)", md: "100vh" }} overflow="hidden">
|
||||
<Flex
|
||||
gap="2"
|
||||
direction="column"
|
||||
@ -104,7 +104,7 @@ function DirectMessagesPage() {
|
||||
Load More
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex gap="2" direction="column" flex={1} hideBelow={!isChatOpen ? "xl" : undefined}>
|
||||
<Flex gap="2" direction="column" flex={1} hideBelow={!isChatOpen ? "xl" : undefined} overflow="hidden">
|
||||
<Outlet />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
139
src/views/dms/message-block.tsx
Normal file
139
src/views/dms/message-block.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { useRef } from "react";
|
||||
import { Box, BoxProps, Card, CardBody, CardFooter, CardHeader, CardProps, Flex } from "@chakra-ui/react";
|
||||
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { getMessageRecipient } from "../../services/direct-messages";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import DecryptPlaceholder from "./decrypt-placeholder";
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import {
|
||||
embedCashuTokens,
|
||||
embedNostrLinks,
|
||||
renderGenericUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
} from "../../components/embed-types";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
import UserAvatar from "../../components/user-avatar";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import NoteZapButton from "../../components/note/note-zap-button";
|
||||
import UserLink from "../../components/user-link";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import useEventReactions from "../../hooks/use-event-reactions";
|
||||
import AddReactionButton from "../../components/note/components/add-reaction-button";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import NoteReactions from "../../components/note/components/note-reactions";
|
||||
|
||||
export function MessageContent({ event, text, children, ...props }: { event: NostrEvent; text: string } & BoxProps) {
|
||||
let content: EmbedableContent = [text];
|
||||
|
||||
content = embedNostrLinks(content);
|
||||
content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderGenericUrl]);
|
||||
|
||||
// cashu
|
||||
content = embedCashuTokens(content);
|
||||
|
||||
return (
|
||||
<TrustProvider event={event}>
|
||||
<Box whiteSpace="pre-wrap" {...props}>
|
||||
{content}
|
||||
{children}
|
||||
</Box>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageBubble({
|
||||
event,
|
||||
showHeader = true,
|
||||
...props
|
||||
}: { event: NostrEvent; showHeader?: boolean } & Omit<CardProps, "children">) {
|
||||
const account = useCurrentAccount()!;
|
||||
const isOwn = account.pubkey === event.pubkey;
|
||||
const reactions = useEventReactions(event.id) ?? [];
|
||||
const hasReactions = reactions.length > 0;
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(event));
|
||||
|
||||
return (
|
||||
<Card {...props} borderRadius="lg" ref={ref}>
|
||||
{showHeader && (
|
||||
<CardHeader px="2" pt="2" pb="0" gap="2" display="flex" alignItems="center">
|
||||
<UserLink pubkey={event.pubkey} fontWeight="bold" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<NoteZapButton event={event} size="xs" ml="auto" variant="ghost" />
|
||||
<AddReactionButton event={event} size="xs" variant="ghost" />
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardBody px="2" py="2">
|
||||
<DecryptPlaceholder
|
||||
data={event.content}
|
||||
pubkey={isOwn ? getMessageRecipient(event) ?? "" : event.pubkey}
|
||||
variant="link"
|
||||
py="4"
|
||||
px="6rem"
|
||||
>
|
||||
{(text) => (
|
||||
<MessageContent event={event} text={text} display="inline">
|
||||
{!hasReactions && (
|
||||
<Flex float="right">
|
||||
{!showHeader && (
|
||||
<>
|
||||
<NoteZapButton event={event} size="xs" ml="auto" variant="ghost" />
|
||||
<AddReactionButton event={event} size="xs" variant="ghost" ml="1" />
|
||||
</>
|
||||
)}
|
||||
<Timestamp timestamp={event.created_at} ml="2" />
|
||||
</Flex>
|
||||
)}
|
||||
</MessageContent>
|
||||
)}
|
||||
</DecryptPlaceholder>
|
||||
</CardBody>
|
||||
{hasReactions && (
|
||||
<CardFooter alignItems="center" display="flex" gap="2" px="2" pt="0" pb="2">
|
||||
<NoteReactions event={event} size="xs" variant="ghost" />
|
||||
<NoteZapButton event={event} size="xs" mr="auto" variant="ghost" />
|
||||
<Timestamp ml="auto" timestamp={event.created_at} />
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MessageBlock({ events }: { events: NostrEvent[] } & Omit<CardProps, "children">) {
|
||||
const lastEvent = events[events.length - 1];
|
||||
const account = useCurrentAccount()!;
|
||||
const isOwn = account.pubkey === lastEvent.pubkey;
|
||||
|
||||
const avatar = <UserAvatar pubkey={lastEvent.pubkey} size="sm" my="1" />;
|
||||
|
||||
return (
|
||||
<Flex direction="row" gap="2" alignItems="flex-end">
|
||||
{!isOwn && avatar}
|
||||
<Flex
|
||||
direction="column-reverse"
|
||||
gap="1"
|
||||
ml={isOwn ? "auto" : 0}
|
||||
mr={isOwn ? 0 : "auto"}
|
||||
maxW="2xl"
|
||||
alignItems={isOwn ? "flex-end" : "flex-start"}
|
||||
overflowX="hidden"
|
||||
overflowY="visible"
|
||||
>
|
||||
{events.map((event, i, arr) => (
|
||||
<MessageBubble
|
||||
key={event.id}
|
||||
event={event}
|
||||
showHeader={i === arr.length - 1}
|
||||
minW={{ base: 0, sm: "sm", md: "md" }}
|
||||
overflow="hidden"
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
{isOwn && avatar}
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
import { useRef } from "react";
|
||||
import { Box, BoxProps, ButtonGroup, Card, CardBody, CardFooter, CardHeader, CardProps, Flex } from "@chakra-ui/react";
|
||||
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { getMessageRecipient } from "../../services/direct-messages";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import DecryptPlaceholder from "./decrypt-placeholder";
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import {
|
||||
embedCashuTokens,
|
||||
embedNostrLinks,
|
||||
renderGenericUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
} from "../../components/embed-types";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
import UserAvatar from "../../components/user-avatar";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import NoteZapButton from "../../components/note/note-zap-button";
|
||||
import UserLink from "../../components/user-link";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import EventReactionButtons from "../../components/event-reactions/event-reactions";
|
||||
import useEventReactions from "../../hooks/use-event-reactions";
|
||||
import AddReactionButton from "../../components/note/components/add-reaction-button";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
|
||||
export function MessageContent({ event, text, children, ...props }: { event: NostrEvent; text: string } & BoxProps) {
|
||||
let content: EmbedableContent = [text];
|
||||
|
||||
content = embedNostrLinks(content);
|
||||
content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderGenericUrl]);
|
||||
|
||||
// cashu
|
||||
content = embedCashuTokens(content);
|
||||
|
||||
return (
|
||||
<TrustProvider event={event}>
|
||||
<Box whiteSpace="pre-wrap" {...props}>
|
||||
{content}
|
||||
{children}
|
||||
</Box>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Message({ event }: { event: NostrEvent } & Omit<CardProps, "children">) {
|
||||
const account = useCurrentAccount()!;
|
||||
const isOwn = account.pubkey === event.pubkey;
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(event));
|
||||
|
||||
const avatar = <UserAvatar pubkey={event.pubkey} size="sm" my="1" />;
|
||||
const reactions = useEventReactions(event.id) ?? [];
|
||||
|
||||
return (
|
||||
<Flex direction="row" gap="2" alignItems="flex-end" ref={ref}>
|
||||
{!isOwn && avatar}
|
||||
<Card variant="outline" w="full" ml={isOwn ? "auto" : 0} mr={isOwn ? 0 : "auto"} maxW="2xl">
|
||||
{!isOwn && (
|
||||
<CardHeader px="2" pt="2" pb="0" gap="2" display="flex" alignItems="center">
|
||||
<UserLink pubkey={event.pubkey} fontWeight="bold" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<NoteZapButton event={event} size="xs" ml="auto" variant="ghost" />
|
||||
<AddReactionButton event={event} size="xs" variant="ghost" />
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardBody px="2" py="2">
|
||||
<DecryptPlaceholder
|
||||
data={event.content}
|
||||
pubkey={isOwn ? getMessageRecipient(event) ?? "" : event.pubkey}
|
||||
variant="link"
|
||||
py="4"
|
||||
>
|
||||
{(text) => (
|
||||
<MessageContent event={event} text={text} display="inline">
|
||||
{reactions.length === 0 && <Timestamp float="right" timestamp={event.created_at} />}
|
||||
</MessageContent>
|
||||
)}
|
||||
</DecryptPlaceholder>
|
||||
</CardBody>
|
||||
{reactions.length > 0 && (
|
||||
<CardFooter alignItems="center" display="flex" gap="2" px="2" pt="0" pb="2">
|
||||
<ButtonGroup size="sm" mr="auto" variant="ghost">
|
||||
<EventReactionButtons event={event} />
|
||||
</ButtonGroup>
|
||||
<Timestamp ml="auto" timestamp={event.created_at} />
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
{isOwn && avatar}
|
||||
</Flex>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user