mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
NIP-31
add thread buttons to DMs
This commit is contained in:
parent
98b4bef45e
commit
ca4d6df8ef
5
.changeset/tall-trains-kick.md
Normal file
5
.changeset/tall-trains-kick.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Support NIP-31 on unknown event kinds
|
@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { Box, Button, Card, CardBody, CardHeader, CardProps, Flex, Link, Text, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
@ -7,9 +8,16 @@ import UserLink from "../../user-link";
|
||||
import { truncatedId } from "../../../helpers/nostr/events";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
|
||||
import { useMemo } from "react";
|
||||
import { embedEmoji, embedNostrHashtags, embedNostrLinks, embedNostrMentions } from "../../embed-types";
|
||||
import { EmbedableContent } from "../../../helpers/embeds";
|
||||
import {
|
||||
embedEmoji,
|
||||
embedNostrHashtags,
|
||||
embedNostrLinks,
|
||||
embedNostrMentions,
|
||||
renderGenericUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
} from "../../embed-types";
|
||||
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { CodeIcon } from "../../icons";
|
||||
import NoteDebugModal from "../../debug-modals/note-debug-modal";
|
||||
@ -18,15 +26,18 @@ export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "ch
|
||||
const debugModal = useDisclosure();
|
||||
const address = getSharableEventAddress(event);
|
||||
|
||||
const alt = event.tags.find((t) => t[0] === "alt")?.[1];
|
||||
const content = useMemo(() => {
|
||||
let jsx: EmbedableContent = [event.content];
|
||||
let jsx: EmbedableContent = [alt || event.content];
|
||||
jsx = embedNostrLinks(jsx);
|
||||
jsx = embedNostrMentions(jsx, event);
|
||||
jsx = embedNostrHashtags(jsx, event);
|
||||
jsx = embedEmoji(jsx, event);
|
||||
|
||||
jsx = embedUrls(jsx, [renderImageUrl, renderVideoUrl, renderGenericUrl]);
|
||||
|
||||
return jsx;
|
||||
}, [event.content]);
|
||||
}, [event.content, alt]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -234,3 +234,5 @@ export const DownloadIcon = Download01;
|
||||
export const TranslateIcon = Translate01;
|
||||
|
||||
export const ChannelsIcon = MessageChatSquare;
|
||||
|
||||
export const ThreadIcon = MessageChatSquare;
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Portal,
|
||||
useBoolean,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
@ -22,7 +23,11 @@ import { draftEventReaction } from "../../../helpers/nostr/reactions";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function AddReactionButton({ event, ...props }: { event: NostrEvent } & Omit<ButtonProps, "children">) {
|
||||
export default function AddReactionButton({
|
||||
event,
|
||||
portal = false,
|
||||
...props
|
||||
}: { event: NostrEvent; portal?: boolean } & Omit<ButtonProps, "children">) {
|
||||
const toast = useToast();
|
||||
const { requestSignature } = useSigningContext();
|
||||
const reactions = useEventReactions(getEventUID(event)) ?? [];
|
||||
@ -47,6 +52,15 @@ export default function AddReactionButton({ event, ...props }: { event: NostrEve
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<ReactionPicker onSelect={addReaction} />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover isLazy isOpen={popover} onOpen={setPopover.on} onClose={setPopover.off}>
|
||||
<PopoverTrigger>
|
||||
@ -60,12 +74,7 @@ export default function AddReactionButton({ event, ...props }: { event: NostrEve
|
||||
{reactions?.length ?? 0}
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<ReactionPicker onSelect={addReaction} />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
{portal ? <Portal>{content}</Portal> : content}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Button, ButtonGroup, Card, Flex, IconButton, useDisclosure } from "@chakra-ui/react";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
import { UNSAFE_DataRouterContext, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { ChevronLeftIcon } from "../../components/icons";
|
||||
import { ChevronLeftIcon, ThreadIcon } from "../../components/icons";
|
||||
import UserAvatar from "../../components/user-avatar";
|
||||
import UserLink from "../../components/user-link";
|
||||
import { isHexKey } from "../../helpers/nip19";
|
||||
@ -16,19 +16,39 @@ 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-page/timeline-action-and-status";
|
||||
import { LightboxProvider } from "../../components/lightbox-provider";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import { useDecryptionContext } from "../../providers/dycryption-provider";
|
||||
import SendMessageForm from "./components/send-message-form";
|
||||
import { groupMessages } from "../../helpers/nostr/dms";
|
||||
import ThreadDrawer from "./components/thread-drawer";
|
||||
import MessageChatSquare from "../../components/icons/message-chat-square";
|
||||
import ThreadsProvider from "./components/thread-provider";
|
||||
import { useRouterMarker } from "../../providers/drawer-sub-view-provider";
|
||||
import TimelineLoader from "../../classes/timeline-loader";
|
||||
|
||||
/** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */
|
||||
const ChatLog = memo(({ timeline }: { timeline: TimelineLoader }) => {
|
||||
const messages = useSubject(timeline.timeline);
|
||||
const filteredMessages = useMemo(
|
||||
() => messages.filter((e) => !e.tags.some((t) => t[0] === "e" && t[3] === "root")),
|
||||
[messages.length],
|
||||
);
|
||||
const grouped = useMemo(() => groupMessages(filteredMessages), [filteredMessages]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{grouped.map((group) => (
|
||||
<MessageBlock key={group.id} messages={group.events} reverse />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
const account = useCurrentAccount()!;
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { getOrCreateContainer, addToQueue, startQueue } = useDecryptionContext();
|
||||
|
||||
const { router } = useContext(UNSAFE_DataRouterContext)!;
|
||||
const marker = useRouterMarker(router);
|
||||
useEffect(() => {
|
||||
@ -38,11 +58,19 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
const account = useCurrentAccount()!;
|
||||
const { getOrCreateContainer, addToQueue, startQueue } = useDecryptionContext();
|
||||
const openDrawerList = useCallback(() => {
|
||||
marker.set(0);
|
||||
navigate(".", { state: { thread: "list" } });
|
||||
}, [marker, navigate]);
|
||||
|
||||
const closeDrawer = useCallback(() => {
|
||||
if (marker.index.current !== null && marker.index.current > 0) {
|
||||
navigate(-marker.index.current);
|
||||
} else navigate(".", { state: { thread: undefined } });
|
||||
marker.reset();
|
||||
}, [marker, navigate]);
|
||||
|
||||
const myInbox = useReadRelayUrls();
|
||||
|
||||
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, myInbox, [
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
@ -56,12 +84,9 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
},
|
||||
]);
|
||||
|
||||
const messages = useSubject(timeline.timeline).filter((e) => !e.tags.some((t) => t[0] === "e" && t[3] === "root"));
|
||||
const grouped = groupMessages(messages);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const decryptAll = async () => {
|
||||
const promises = messages
|
||||
const promises = timeline.timeline.value
|
||||
.map((message) => {
|
||||
const container = getOrCreateContainer(pubkey, message.content);
|
||||
if (container.plaintext.value === undefined) return addToQueue(container);
|
||||
@ -99,34 +124,18 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
<IconButton
|
||||
aria-label="Threads"
|
||||
title="Threads"
|
||||
icon={<MessageChatSquare boxSize={5} />}
|
||||
onClick={() => {
|
||||
marker.set(0);
|
||||
navigate(".", { state: { thread: "list" } });
|
||||
}}
|
||||
icon={<ThreadIcon boxSize={5} />}
|
||||
onClick={openDrawerList}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Card>
|
||||
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column-reverse" gap="2" py="4" px="2">
|
||||
<LightboxProvider>
|
||||
{grouped.map((group) => (
|
||||
<MessageBlock key={group.id} messages={group.events} reverse />
|
||||
))}
|
||||
</LightboxProvider>
|
||||
<ChatLog timeline={timeline} />
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</Flex>
|
||||
<SendMessageForm flexShrink={0} pubkey={pubkey} />
|
||||
{location.state?.thread && (
|
||||
<ThreadDrawer
|
||||
isOpen
|
||||
onClose={() => {
|
||||
if (marker.index.current !== null && marker.index.current > 0) {
|
||||
navigate(-marker.index.current);
|
||||
} else navigate(".", { state: { thread: undefined } });
|
||||
}}
|
||||
threadId={location.state.thread}
|
||||
pubkey={pubkey}
|
||||
/>
|
||||
<ThreadDrawer isOpen onClose={closeDrawer} threadId={location.state.thread} pubkey={pubkey} />
|
||||
)}
|
||||
</IntersectionObserverProvider>
|
||||
</ThreadsProvider>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { memo } from "react";
|
||||
import { CardProps, Flex } from "@chakra-ui/react";
|
||||
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
@ -7,30 +8,30 @@ import MessageBubble, { MessageBubbleProps } from "./message-bubble";
|
||||
import { useThreadsContext } from "./thread-provider";
|
||||
import ThreadButton from "./thread-button";
|
||||
|
||||
function MessageBubbleWithThread({ message, ...props }: MessageBubbleProps) {
|
||||
function MessageBubbleWithThread({ message, showThreadButton = true, ...props }: MessageBubbleProps) {
|
||||
const { threads } = useThreadsContext();
|
||||
const thread = threads[message.id];
|
||||
|
||||
return (
|
||||
<>
|
||||
{thread && <ThreadButton thread={thread} />}
|
||||
<MessageBubble message={message} {...props} />
|
||||
{showThreadButton && !!thread && <ThreadButton thread={thread} />}
|
||||
<MessageBubble message={message} showThreadButton={showThreadButton && !thread} {...props} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MessageBlock({
|
||||
function MessageBlock({
|
||||
messages,
|
||||
showThreadButtons = true,
|
||||
showThreadButton = true,
|
||||
reverse = false,
|
||||
}: { messages: NostrEvent[]; showThreadButtons?: boolean; reverse?: boolean } & Omit<CardProps, "children">) {
|
||||
}: { messages: NostrEvent[]; showThreadButton?: boolean; reverse?: boolean } & Omit<CardProps, "children">) {
|
||||
const lastEvent = messages[messages.length - 1];
|
||||
const account = useCurrentAccount()!;
|
||||
const isOwn = account.pubkey === lastEvent.pubkey;
|
||||
|
||||
const avatar = <UserAvatar pubkey={lastEvent.pubkey} size="sm" my="1" />;
|
||||
|
||||
const MessageBubbleComponent = showThreadButtons ? MessageBubbleWithThread : MessageBubble;
|
||||
const MessageBubbleComponent = showThreadButton ? MessageBubbleWithThread : MessageBubble;
|
||||
|
||||
return (
|
||||
<Flex direction="row" gap="2" alignItems="flex-end">
|
||||
@ -51,7 +52,9 @@ export default function MessageBlock({
|
||||
message={message}
|
||||
showHeader={reverse ? i === arr.length - 1 : i === 0}
|
||||
minW={{ base: 0, sm: "sm", md: "md" }}
|
||||
maxW="full"
|
||||
overflow="hidden"
|
||||
showThreadButton={showThreadButton}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
@ -59,3 +62,5 @@ export default function MessageBlock({
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MessageBlock);
|
||||
|
@ -1,5 +1,16 @@
|
||||
import { useRef } from "react";
|
||||
import { Box, BoxProps, Card, CardBody, CardFooter, CardHeader, CardProps, Flex } from "@chakra-ui/react";
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import DecryptPlaceholder from "./decrypt-placeholder";
|
||||
@ -20,7 +31,32 @@ 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";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { ThreadIcon } from "../../../components/icons";
|
||||
import EventReactionButtons from "../../../components/event-reactions/event-reactions";
|
||||
import { LightboxProvider } from "../../../components/lightbox-provider";
|
||||
|
||||
export function IconThreadButton({
|
||||
event,
|
||||
...props
|
||||
}: { event: NostrEvent } & Omit<IconButtonProps, "aria-label" | "icon">) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const onClick = () => {
|
||||
navigate(`.`, { state: { ...location.state, thread: event.id } });
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={<ThreadIcon />}
|
||||
onClick={onClick}
|
||||
aria-label="Reply in thread"
|
||||
title="Reply in thread"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessageContent({ event, text, children, ...props }: { event: NostrEvent; text: string } & BoxProps) {
|
||||
let content: EmbedableContent = [text];
|
||||
@ -33,31 +69,55 @@ export function MessageContent({ event, text, children, ...props }: { event: Nos
|
||||
|
||||
return (
|
||||
<TrustProvider event={event}>
|
||||
<Box whiteSpace="pre-wrap" {...props}>
|
||||
{content}
|
||||
{children}
|
||||
</Box>
|
||||
<LightboxProvider>
|
||||
<Box whiteSpace="pre-wrap" {...props}>
|
||||
{content}
|
||||
{children}
|
||||
</Box>
|
||||
</LightboxProvider>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export type MessageBubbleProps = { message: NostrEvent; showHeader?: boolean } & Omit<CardProps, "children">;
|
||||
export type MessageBubbleProps = { message: NostrEvent; showHeader?: boolean; showThreadButton?: boolean } & Omit<
|
||||
CardProps,
|
||||
"children"
|
||||
>;
|
||||
|
||||
export default function MessageBubble({ message, showHeader = true, ...props }: MessageBubbleProps) {
|
||||
export default function MessageBubble({
|
||||
message,
|
||||
showHeader = true,
|
||||
showThreadButton = true,
|
||||
...props
|
||||
}: MessageBubbleProps) {
|
||||
const reactions = useEventReactions(message.id) ?? [];
|
||||
const hasReactions = reactions.length > 0;
|
||||
|
||||
let actionPosition = showHeader ? "header" : "inline";
|
||||
if (hasReactions && actionPosition === "inline") actionPosition = "footer";
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(message));
|
||||
|
||||
const actions = (
|
||||
<>
|
||||
<NoteZapButton event={message} />
|
||||
<AddReactionButton event={message} portal />
|
||||
{showThreadButton && <IconThreadButton event={message} />}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card {...props} borderRadius="lg" ref={ref}>
|
||||
{showHeader && (
|
||||
<CardHeader px="2" pt="2" pb="0" gap="2" display="flex" alignItems="center">
|
||||
<UserLink pubkey={message.pubkey} fontWeight="bold" />
|
||||
<UserDnsIdentityIcon pubkey={message.pubkey} onlyIcon />
|
||||
<NoteZapButton event={message} size="xs" ml="auto" variant="ghost" />
|
||||
<AddReactionButton event={message} size="xs" variant="ghost" />
|
||||
{actionPosition === "header" && (
|
||||
<ButtonGroup size="xs" variant="ghost" ml="auto">
|
||||
{actions}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardBody px="2" py="2">
|
||||
@ -65,15 +125,10 @@ export default function MessageBubble({ message, showHeader = true, ...props }:
|
||||
{(plaintext) => (
|
||||
<MessageContent event={message} text={plaintext} display="inline">
|
||||
{!hasReactions && (
|
||||
<Flex float="right">
|
||||
{!showHeader && (
|
||||
<>
|
||||
<NoteZapButton event={message} size="xs" ml="2" variant="ghost" />
|
||||
<AddReactionButton event={message} size="xs" variant="ghost" ml="1" />
|
||||
</>
|
||||
)}
|
||||
<ButtonGroup size="xs" variant="ghost" float="right">
|
||||
{actionPosition === "inline" && actions}
|
||||
<Timestamp timestamp={message.created_at} ml="2" />
|
||||
</Flex>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</MessageContent>
|
||||
)}
|
||||
@ -81,8 +136,10 @@ export default function MessageBubble({ message, showHeader = true, ...props }:
|
||||
</CardBody>
|
||||
{hasReactions && (
|
||||
<CardFooter alignItems="center" display="flex" gap="2" px="2" pt="0" pb="2">
|
||||
<NoteReactions event={message} size="xs" variant="ghost" />
|
||||
<NoteZapButton event={message} size="xs" mr="auto" variant="ghost" />
|
||||
<ButtonGroup size="xs" variant="ghost">
|
||||
{actionPosition === "footer" ? actions : <AddReactionButton event={message} portal />}
|
||||
<EventReactionButtons event={message} />
|
||||
</ButtonGroup>
|
||||
<Timestamp ml="auto" timestamp={message.created_at} />
|
||||
</CardFooter>
|
||||
)}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Button } from "@chakra-ui/react";
|
||||
import { Button, IconButton } from "@chakra-ui/react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import UserAvatar from "../../../components/user-avatar";
|
||||
import { Thread } from "./thread-provider";
|
||||
import { ChevronRightIcon } from "../../../components/icons";
|
||||
import { ChevronRightIcon, ThreadIcon } from "../../../components/icons";
|
||||
import { IconButtonProps } from "yet-another-react-lightbox";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
|
||||
export default function ThreadButton({ thread }: { thread: Thread }) {
|
||||
const navigate = useNavigate();
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@ -25,12 +26,9 @@ import Timestamp from "../../../components/timestamp";
|
||||
import { Thread, useThreadsContext } from "./thread-provider";
|
||||
import ThreadButton from "./thread-button";
|
||||
import MessageBlock from "./message-block";
|
||||
import { LightboxProvider } from "../../../components/lightbox-provider";
|
||||
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
|
||||
import SendMessageForm from "./send-message-form";
|
||||
import { groupMessages } from "../../../helpers/nostr/dms";
|
||||
import { useDecryptionContext } from "../../../providers/dycryption-provider";
|
||||
import { useState } from "react";
|
||||
|
||||
function MessagePreview({ message, ...props }: { message: NostrEvent } & Omit<TextProps, "children">) {
|
||||
return (
|
||||
@ -84,12 +82,10 @@ function ThreadMessages({ thread, pubkey }: { thread: Thread; pubkey: string })
|
||||
return (
|
||||
<>
|
||||
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column" gap="2">
|
||||
{thread.root && <MessageBlock messages={[thread.root]} showThreadButtons={false} />}
|
||||
<LightboxProvider>
|
||||
{grouped.map((group) => (
|
||||
<MessageBlock key={group.id} messages={group.events} showThreadButtons={false} />
|
||||
))}
|
||||
</LightboxProvider>
|
||||
{thread.root && <MessageBlock messages={[thread.root]} showThreadButton={false} />}
|
||||
{grouped.map((group) => (
|
||||
<MessageBlock key={group.id} messages={group.events} showThreadButton={false} />
|
||||
))}
|
||||
</Flex>
|
||||
<SendMessageForm flexShrink={0} pubkey={pubkey} rootId={thread.rootId} />
|
||||
</>
|
||||
@ -101,7 +97,7 @@ export default function ThreadDrawer({
|
||||
pubkey,
|
||||
...props
|
||||
}: Omit<DrawerProps, "children"> & { threadId: string; pubkey: string }) {
|
||||
const { threads } = useThreadsContext();
|
||||
const { threads, getRoot } = useThreadsContext();
|
||||
const { startQueue, getOrCreateContainer, addToQueue } = useDecryptionContext();
|
||||
|
||||
const thread = threads[threadId];
|
||||
@ -129,8 +125,9 @@ export default function ThreadDrawer({
|
||||
|
||||
const renderContent = () => {
|
||||
if (threadId === "list") return <ListThreads />;
|
||||
if (!thread) return <Spinner />;
|
||||
return <ThreadMessages thread={thread} pubkey={pubkey} />;
|
||||
if (!thread) {
|
||||
return <ThreadMessages thread={{ rootId: threadId, messages: [], root: getRoot(threadId) }} pubkey={pubkey} />;
|
||||
} else return <ThreadMessages thread={thread} pubkey={pubkey} />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren, createContext, useContext } from "react";
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
|
||||
|
||||
import TimelineLoader from "../../../classes/timeline-loader";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
@ -11,8 +11,14 @@ export type Thread = {
|
||||
};
|
||||
type ThreadsContextType = {
|
||||
threads: Record<string, Thread>;
|
||||
getRoot: (id: string) => NostrEvent | undefined;
|
||||
};
|
||||
const ThreadsContext = createContext<ThreadsContextType>({ threads: {} });
|
||||
const ThreadsContext = createContext<ThreadsContextType>({
|
||||
threads: {},
|
||||
getRoot: (id: string) => {
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
export function useThreadsContext() {
|
||||
return useContext(ThreadsContext);
|
||||
@ -21,23 +27,32 @@ export function useThreadsContext() {
|
||||
export default function ThreadsProvider({ timeline, children }: { timeline: TimelineLoader } & PropsWithChildren) {
|
||||
const messages = useSubject(timeline.timeline);
|
||||
|
||||
const groupedByRoot: Record<string, NostrEvent[]> = {};
|
||||
for (const message of messages) {
|
||||
const rootId = message.tags.find((t) => t[0] === "e" && t[3] === "root")?.[1];
|
||||
if (rootId) {
|
||||
if (!groupedByRoot[rootId]) groupedByRoot[rootId] = [];
|
||||
groupedByRoot[rootId].push(message);
|
||||
const threads = useMemo(() => {
|
||||
const grouped: Record<string, Thread> = {};
|
||||
for (const message of messages) {
|
||||
const rootId = message.tags.find((t) => t[0] === "e" && t[3] === "root")?.[1];
|
||||
if (rootId) {
|
||||
if (!grouped[rootId]) {
|
||||
grouped[rootId] = {
|
||||
messages: [],
|
||||
rootId,
|
||||
root: timeline.events.getEvent(rootId),
|
||||
};
|
||||
}
|
||||
grouped[rootId].messages.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return grouped;
|
||||
}, [messages.length, timeline.events]);
|
||||
|
||||
const threads: Record<string, Thread> = {};
|
||||
for (const [rootId, messages] of Object.entries(groupedByRoot)) {
|
||||
threads[rootId] = {
|
||||
messages,
|
||||
rootId,
|
||||
root: timeline.events.getEvent(rootId),
|
||||
};
|
||||
}
|
||||
const getRoot = useCallback(
|
||||
(id: string) => {
|
||||
return timeline.events.getEvent(id);
|
||||
},
|
||||
[timeline.events],
|
||||
);
|
||||
|
||||
return <ThreadsContext.Provider value={{ threads }}>{children}</ThreadsContext.Provider>;
|
||||
const context = useMemo(() => ({ threads, getRoot }), [threads, getRoot]);
|
||||
|
||||
return <ThreadsContext.Provider value={context}>{children}</ThreadsContext.Provider>;
|
||||
}
|
||||
|
@ -110,9 +110,11 @@ export default function LoginStartView() {
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" flexShrink={0} alignItems="center">
|
||||
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="sm" colorScheme="primary">
|
||||
Sign in with extension
|
||||
</Button>
|
||||
{window.nostr && (
|
||||
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="sm" colorScheme="primary">
|
||||
Sign in with extension
|
||||
</Button>
|
||||
)}
|
||||
<Button as={RouterLink} to="./nostr-connect" state={location.state} w="sm" colorScheme="blue">
|
||||
Nostr Connect (NIP-46)
|
||||
</Button>
|
||||
|
Loading…
x
Reference in New Issue
Block a user