mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-28 18:53:47 +01:00
Rebuild direct message chat view using timeline loader
This commit is contained in:
parent
e6b773980a
commit
bdc1c98d78
5
.changeset/selfish-pants-design.md
Normal file
5
.changeset/selfish-pants-design.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Rebuild direct message chat view using timeline loader
|
5
.changeset/slimy-pandas-check.md
Normal file
5
.changeset/slimy-pandas-check.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Dont show multiple images on open-graph link card
|
@ -25,8 +25,8 @@ import RelaysView from "./views/relays";
|
||||
import LoginNip05View from "./views/login/nip05";
|
||||
import LoginNsecView from "./views/login/nsec";
|
||||
import UserZapsTab from "./views/user/zaps";
|
||||
import DirectMessagesView from "./views/dm";
|
||||
import DirectMessageChatView from "./views/dm/chat";
|
||||
import DirectMessagesView from "./views/messages";
|
||||
import DirectMessageChatView from "./views/messages/chat";
|
||||
import NostrLinkView from "./views/link";
|
||||
import UserReportsTab from "./views/user/reports";
|
||||
import appSettings from "./services/app-settings";
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Box, CardProps, Code, Heading, Image, Link, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
||||
import { Box, CardProps, Heading, Image, Link, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
||||
import useOpenGraphData from "../hooks/use-open-graph-data";
|
||||
|
||||
export default function OpenGraphCard({ url, ...props }: { url: URL } & Omit<CardProps, "children">) {
|
||||
const { value: data, loading } = useOpenGraphData(url);
|
||||
const { value: data } = useOpenGraphData(url);
|
||||
|
||||
const link = (
|
||||
<Link href={url.toString()} isExternal color="blue.500">
|
||||
@ -14,14 +14,12 @@ export default function OpenGraphCard({ url, ...props }: { url: URL } & Omit<Car
|
||||
|
||||
return (
|
||||
<LinkBox borderRadius="lg" borderWidth={1} overflow="hidden" {...props}>
|
||||
{data.ogImage?.map((ogImage) => (
|
||||
<Image key={ogImage.url} src={ogImage.url} mx="auto" />
|
||||
))}
|
||||
{data.ogImage?.length === 1 && <Image key={data.ogImage[0].url} src={data.ogImage[0].url} mx="auto" />}
|
||||
|
||||
<Box m="2" mt="4">
|
||||
<Heading size="sm" my="2">
|
||||
<LinkOverlay href={url.toString()} isExternal>
|
||||
{data.ogTitle ?? data.dcTitle}
|
||||
{data.ogTitle?.trim() ?? data.dcTitle?.trim()}
|
||||
</LinkOverlay>
|
||||
</Heading>
|
||||
<Text isTruncated>{data.ogDescription || data.dcDescription}</Text>
|
||||
|
@ -94,7 +94,11 @@ class DirectMessagesService {
|
||||
const account = accountService.current.value;
|
||||
if (!account) return;
|
||||
|
||||
if (this.incomingSub.query?.since && dayjs.unix(this.incomingSub.query.since).isBefore(from)) {
|
||||
if (
|
||||
!Array.isArray(this.incomingSub.query) &&
|
||||
this.incomingSub.query?.since &&
|
||||
dayjs.unix(this.incomingSub.query.since).isBefore(from)
|
||||
) {
|
||||
// "since" is already set on the subscription and its older than "from"
|
||||
return;
|
||||
}
|
||||
|
@ -1,138 +0,0 @@
|
||||
import { Box, Button, Card, CardBody, CardProps, Flex, IconButton, Spacer, Text, Textarea } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, Navigate, useParams } from "react-router-dom";
|
||||
import { nostrPostAction } from "../../classes/nostr-post-action";
|
||||
import { ArrowLeftSIcon } from "../../components/icons";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { normalizeToHex } from "../../helpers/nip19";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import directMessagesService, { getMessageRecipient } from "../../services/direct-messages";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import DecryptPlaceholder from "./decrypt-placeholder";
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import { embedNostrLinks, renderGenericUrl, renderImageUrl, renderVideoUrl } from "../../components/embed-types";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
|
||||
function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
|
||||
let content: EmbedableContent = [text];
|
||||
|
||||
content = embedNostrLinks(content);
|
||||
|
||||
content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderGenericUrl]);
|
||||
|
||||
return <Box whiteSpace="pre-wrap">{content}</Box>;
|
||||
}
|
||||
|
||||
function Message({ event }: { event: NostrEvent } & Omit<CardProps, "children">) {
|
||||
const account = useCurrentAccount()!;
|
||||
const isOwnMessage = account.pubkey === event.pubkey;
|
||||
|
||||
return (
|
||||
<Flex direction="column">
|
||||
<Text size="sm" textAlign={isOwnMessage ? "right" : "left"} px="2">
|
||||
{dayjs.unix(event.created_at).fromNow()}
|
||||
</Text>
|
||||
<Card size="sm" mr={isOwnMessage ? 0 : "8"} ml={isOwnMessage ? "8" : 0}>
|
||||
<CardBody position="relative">
|
||||
<DecryptPlaceholder
|
||||
data={event.content}
|
||||
pubkey={isOwnMessage ? getMessageRecipient(event) ?? "" : event.pubkey}
|
||||
>
|
||||
{(text) => <MessageContent event={event} text={text} />}
|
||||
</DecryptPlaceholder>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function DirectMessageChatPage() {
|
||||
const { key } = useParams();
|
||||
if (!key) return <Navigate to="/" />;
|
||||
const pubkey = normalizeToHex(key);
|
||||
if (!pubkey) throw new Error("invalid pubkey");
|
||||
|
||||
const { requestEncrypt, requestSignature } = useSigningContext();
|
||||
const isMobile = useIsMobile();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [from, setFrom] = useState(dayjs().subtract(1, "week"));
|
||||
const [content, setContent] = useState<string>("");
|
||||
|
||||
useEffect(() => directMessagesService.loadDateRange(from), [from]);
|
||||
|
||||
const loadMore = () => {
|
||||
setLoading(true);
|
||||
setFrom((date) => dayjs(date).subtract(1, "week"));
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const subject = useMemo(() => directMessagesService.getUserMessages(pubkey), [pubkey]);
|
||||
const messages = useSubject(subject);
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!content) return;
|
||||
const encrypted = await requestEncrypt(content, pubkey);
|
||||
if (!encrypted) return;
|
||||
const event: DraftNostrEvent = {
|
||||
kind: Kind.EncryptedDirectMessage,
|
||||
content: encrypted,
|
||||
tags: [["p", pubkey]],
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
const signed = await requestSignature(event);
|
||||
if (!signed) return;
|
||||
const writeRelays = clientRelaysService.getWriteUrls();
|
||||
nostrPostAction(writeRelays, signed);
|
||||
setContent("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex height="100%" overflow="hidden" direction="column">
|
||||
<Card size="sm" flexShrink={0}>
|
||||
<CardBody display="flex" gap="2" alignItems="center">
|
||||
<IconButton
|
||||
as={Link}
|
||||
variant="ghost"
|
||||
icon={<ArrowLeftSIcon />}
|
||||
aria-label="Back"
|
||||
to="/dm"
|
||||
size={isMobile ? "sm" : "md"}
|
||||
/>
|
||||
<UserAvatar pubkey={pubkey} size={isMobile ? "sm" : "md"} />
|
||||
<UserLink pubkey={pubkey} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Flex flex={1} overflowX="hidden" overflowY="scroll" direction="column" gap="4" py="4">
|
||||
<Spacer height="100vh" />
|
||||
<Button onClick={loadMore} mx="auto" flexShrink={0} isLoading={loading}>
|
||||
Load More
|
||||
</Button>
|
||||
{[...messages].reverse().map((event) => (
|
||||
<Message key={event.id} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
<Flex shrink={0}>
|
||||
<Textarea value={content} onChange={(e) => setContent(e.target.value)} />
|
||||
<Button isDisabled={!content} onClick={sendMessage}>
|
||||
Send
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default function DirectMessageChatView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<DirectMessageChatPage />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
112
src/views/messages/chat.tsx
Normal file
112
src/views/messages/chat.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import { Button, Card, CardBody, Flex, IconButton, Textarea } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { useRef, useState } from "react";
|
||||
import { Link, Navigate, useParams } from "react-router-dom";
|
||||
import { nostrPostAction } from "../../classes/nostr-post-action";
|
||||
import { ArrowLeftSIcon } from "../../components/icons";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { normalizeToHex } from "../../helpers/nip19";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import { Message } from "./message";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { truncatedId } from "../../helpers/nostr-event";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import TimelineActionAndStatus from "../../components/timeline-action-and-status";
|
||||
|
||||
function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
const isMobile = useIsMobile();
|
||||
const account = useCurrentAccount()!;
|
||||
const { requestEncrypt, requestSignature } = useSigningContext();
|
||||
const [content, setContent] = useState<string>("");
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
|
||||
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-${truncatedId(account.pubkey)}-messages`, readRelays, [
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
"#p": [account.pubkey],
|
||||
authors: [pubkey],
|
||||
},
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
"#p": [pubkey],
|
||||
authors: [account.pubkey],
|
||||
},
|
||||
]);
|
||||
|
||||
const messages = useSubject(timeline.timeline);
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!content) return;
|
||||
const encrypted = await requestEncrypt(content, pubkey);
|
||||
if (!encrypted) return;
|
||||
const event: DraftNostrEvent = {
|
||||
kind: Kind.EncryptedDirectMessage,
|
||||
content: encrypted,
|
||||
tags: [["p", pubkey]],
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
const signed = await requestSignature(event);
|
||||
if (!signed) return;
|
||||
const writeRelays = clientRelaysService.getWriteUrls();
|
||||
nostrPostAction(writeRelays, signed);
|
||||
setContent("");
|
||||
};
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider root={scrollBox} callback={callback}>
|
||||
<Flex height="100%" overflow="hidden" direction="column">
|
||||
<Card size="sm" flexShrink={0}>
|
||||
<CardBody display="flex" gap="2" alignItems="center">
|
||||
<IconButton
|
||||
as={Link}
|
||||
variant="ghost"
|
||||
icon={<ArrowLeftSIcon />}
|
||||
aria-label="Back"
|
||||
to="/dm"
|
||||
size={isMobile ? "sm" : "md"}
|
||||
/>
|
||||
<UserAvatar pubkey={pubkey} size={isMobile ? "sm" : "md"} />
|
||||
<UserLink pubkey={pubkey} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Flex flex={1} overflowX="hidden" overflowY="scroll" direction="column-reverse" gap="4" py="4">
|
||||
{[...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>
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
export default function DirectMessageChatView() {
|
||||
const { key } = useParams();
|
||||
if (!key) return <Navigate to="/" />;
|
||||
const pubkey = normalizeToHex(key);
|
||||
if (!pubkey) throw new Error("invalid pubkey");
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<DirectMessageChatPage pubkey={pubkey} />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
@ -14,7 +14,7 @@ import {
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
|
51
src/views/messages/message.tsx
Normal file
51
src/views/messages/message.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Box, Card, CardBody, CardHeader, CardProps, Flex, Heading, Text } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
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 { embedNostrLinks, renderGenericUrl, renderImageUrl, renderVideoUrl } from "../../components/embed-types";
|
||||
import { useRef } from "react";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
|
||||
export function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
|
||||
let content: EmbedableContent = [text];
|
||||
|
||||
content = embedNostrLinks(content);
|
||||
content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderGenericUrl]);
|
||||
|
||||
return <Box whiteSpace="pre-wrap">{content}</Box>;
|
||||
}
|
||||
|
||||
export function Message({ event }: { event: NostrEvent } & Omit<CardProps, "children">) {
|
||||
const account = useCurrentAccount()!;
|
||||
const isOwnMessage = account.pubkey === event.pubkey;
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, event.id);
|
||||
|
||||
return (
|
||||
<Flex direction="column" ref={ref}>
|
||||
<Card size="sm">
|
||||
<CardHeader display="flex" gap="2" alignItems="center" pb="0">
|
||||
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||
<Heading size="md">
|
||||
<UserLink pubkey={event.pubkey} />
|
||||
</Heading>
|
||||
<Text ml="auto">{dayjs.unix(event.created_at).fromNow()}</Text>
|
||||
</CardHeader>
|
||||
<CardBody position="relative">
|
||||
<DecryptPlaceholder
|
||||
data={event.content}
|
||||
pubkey={isOwnMessage ? getMessageRecipient(event) ?? "" : event.pubkey}
|
||||
>
|
||||
{(text) => <MessageContent event={event} text={text} />}
|
||||
</DecryptPlaceholder>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Flex>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user