diff --git a/.changeset/new-planes-warn.md b/.changeset/new-planes-warn.md new file mode 100644 index 000000000..fc51e5c04 --- /dev/null +++ b/.changeset/new-planes-warn.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Improve channel message layout diff --git a/src/components/embed-event/event-types/embedded-dm.tsx b/src/components/embed-event/event-types/embedded-dm.tsx index 7c85f901a..8cb2ef565 100644 --- a/src/components/embed-event/event-types/embedded-dm.tsx +++ b/src/components/embed-event/event-types/embedded-dm.tsx @@ -8,7 +8,7 @@ import Timestamp from "../../timestamp"; import DecryptPlaceholder from "../../../views/dms/components/decrypt-placeholder"; import useCurrentAccount from "../../../hooks/use-current-account"; import { getDMRecipient, getDMSender } from "../../../helpers/nostr/dms"; -import { MessageContent } from "../../../views/dms/components/message-bubble"; +import DirectMessageContent from "../../../views/dms/components/direct-message-content"; export default function EmbeddedDM({ dm, ...props }: Omit & { dm: NostrEvent }) { const account = useCurrentAccount(); @@ -31,7 +31,7 @@ export default function EmbeddedDM({ dm, ...props }: Omit {(sender === account?.pubkey || receiver === account?.pubkey) && ( - {(plaintext) => } + {(plaintext) => } )} diff --git a/src/views/dms/components/message-block.tsx b/src/components/message-block.tsx similarity index 73% rename from src/views/dms/components/message-block.tsx rename to src/components/message-block.tsx index 6a613de2d..3134ad802 100644 --- a/src/views/dms/components/message-block.tsx +++ b/src/components/message-block.tsx @@ -1,12 +1,11 @@ -import { memo } from "react"; import { CardProps, Flex } from "@chakra-ui/react"; -import useCurrentAccount from "../../../hooks/use-current-account"; -import { NostrEvent } from "../../../types/nostr-event"; -import UserAvatar from "../../../components/user-avatar"; +import useCurrentAccount from "../hooks/use-current-account"; +import { NostrEvent } from "../types/nostr-event"; +import UserAvatar from "./user-avatar"; import MessageBubble, { MessageBubbleProps } from "./message-bubble"; -import { useThreadsContext } from "./thread-provider"; -import ThreadButton from "./thread-button"; +import { useThreadsContext } from "../providers/thread-provider"; +import ThreadButton from "../views/dms/components/thread-button"; function MessageBubbleWithThread({ message, showThreadButton = true, ...props }: MessageBubbleProps) { const { threads } = useThreadsContext(); @@ -20,11 +19,19 @@ function MessageBubbleWithThread({ message, showThreadButton = true, ...props }: ); } -function MessageBlock({ +export type MessageBlockProps = Omit & { + messages: NostrEvent[]; + showThreadButton?: boolean; + reverse?: boolean; + renderContent: MessageBubbleProps["renderContent"]; +}; + +export default function MessageBlock({ messages, showThreadButton = true, reverse = false, -}: { messages: NostrEvent[]; showThreadButton?: boolean; reverse?: boolean } & Omit) { + renderContent, +}: MessageBlockProps) { const lastEvent = messages[messages.length - 1]; const account = useCurrentAccount()!; const isOwn = account.pubkey === lastEvent.pubkey; @@ -55,6 +62,7 @@ function MessageBlock({ maxW="full" overflow="hidden" showThreadButton={showThreadButton} + renderContent={renderContent} /> ))} @@ -62,5 +70,3 @@ function MessageBlock({ ); } - -export default memo(MessageBlock); diff --git a/src/components/message-bubble.tsx b/src/components/message-bubble.tsx new file mode 100644 index 000000000..707252ed3 --- /dev/null +++ b/src/components/message-bubble.tsx @@ -0,0 +1,82 @@ +import { ReactNode, useRef } from "react"; +import { ButtonGroup, Card, CardBody, CardFooter, CardHeader, CardProps } from "@chakra-ui/react"; + +import { NostrEvent } from "../types/nostr-event"; +import { useRegisterIntersectionEntity } from "../providers/intersection-observer"; +import { getEventUID } from "../helpers/nostr/events"; +import Timestamp from "./timestamp"; +import NoteZapButton from "./note/note-zap-button"; +import UserLink from "./user-link"; +import { UserDnsIdentityIcon } from "./user-dns-identity-icon"; +import useEventReactions from "../hooks/use-event-reactions"; +import AddReactionButton from "./note/components/add-reaction-button"; +import EventReactionButtons from "./event-reactions/event-reactions"; +import { IconThreadButton } from "../views/dms/components/thread-button"; + +export type MessageBubbleProps = { + message: NostrEvent; + showHeader?: boolean; + showThreadButton?: boolean; + renderContent: (message: NostrEvent, inlineButtons: ReactNode | null) => ReactNode; +} & Omit; + +export default function MessageBubble({ + message, + showHeader = true, + showThreadButton = true, + renderContent, + ...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(null); + useRegisterIntersectionEntity(ref, getEventUID(message)); + + const actions = ( + <> + + + {showThreadButton && } + + ); + + return ( + + {showHeader && ( + + + + {actionPosition === "header" && ( + + {actions} + + )} + + )} + + {renderContent( + message, + !hasReactions ? ( + + {actionPosition === "inline" && actions} + + + ) : null, + )} + + {hasReactions && ( + + + {actionPosition === "footer" ? actions : } + + + + + )} + + ); +} diff --git a/src/views/dms/components/thread-provider.tsx b/src/providers/thread-provider.tsx similarity index 89% rename from src/views/dms/components/thread-provider.tsx rename to src/providers/thread-provider.tsx index e720295ec..931167717 100644 --- a/src/views/dms/components/thread-provider.tsx +++ b/src/providers/thread-provider.tsx @@ -1,8 +1,8 @@ import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react"; -import TimelineLoader from "../../../classes/timeline-loader"; -import { NostrEvent } from "../../../types/nostr-event"; -import useSubject from "../../../hooks/use-subject"; +import TimelineLoader from "../classes/timeline-loader"; +import { NostrEvent } from "../types/nostr-event"; +import useSubject from "../hooks/use-subject"; export type Thread = { root?: NostrEvent; diff --git a/src/views/channels/channel.tsx b/src/views/channels/channel.tsx index 41948e7bc..da6e0b023 100644 --- a/src/views/channels/channel.tsx +++ b/src/views/channels/channel.tsx @@ -1,6 +1,7 @@ -import { useMemo } from "react"; +import { memo, useCallback, useMemo } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Button, Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react"; +import { Kind } from "nostr-tools"; import { safeDecode } from "../../helpers/nip19"; import useSingleEvent from "../../hooks/use-single-event"; @@ -12,8 +13,35 @@ import { ChevronLeftIcon } from "../../components/icons"; import RelaySelectionButton from "../../components/relay-selection/relay-selection-button"; import ChannelMetadataDrawer from "./components/channel-metadata-drawer"; import ChannelJoinButton from "./components/channel-join-button"; -import ChannelChatLog from "./components/channel-chat-log"; import ChannelMenu from "./components/channel-menu"; +import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter"; +import useTimelineLoader from "../../hooks/use-timeline-loader"; +import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; +import IntersectionObserverProvider from "../../providers/intersection-observer"; +import ThreadsProvider from "../../providers/thread-provider"; +import TimelineLoader from "../../classes/timeline-loader"; +import useSubject from "../../hooks/use-subject"; +import { groupMessages } from "../../helpers/nostr/dms"; +import ChannelMessageBlock from "./components/channel-message-block"; +import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; +import ChannelMessageForm from "./components/send-message-form"; + +const ChannelChatLog = memo(({ timeline, channel }: { timeline: TimelineLoader; channel: NostrEvent }) => { + const messages = useSubject(timeline.timeline); + const filteredMessages = useMemo( + () => messages.filter((e) => !e.tags.some((t) => t[0] === "e" && t[1] !== channel.id && t[3] === "root")), + [messages.length, channel.id], + ); + const grouped = useMemo(() => groupMessages(filteredMessages), [filteredMessages]); + + return ( + <> + {grouped.map((group) => ( + + ))} + + ); +}); function ChannelPage({ channel }: { channel: NostrEvent }) { const navigate = useNavigate(); @@ -21,26 +49,62 @@ function ChannelPage({ channel }: { channel: NostrEvent }) { const { metadata } = useChannelMetadata(channel.id, relays); const drawer = useDisclosure(); + const clientMuteFilter = useClientSideMuteFilter(); + const eventFilter = useCallback( + (e: NostrEvent) => { + if (clientMuteFilter(e)) return false; + return true; + }, + [clientMuteFilter], + ); + const timeline = useTimelineLoader( + `${channel.id}-chat-messages`, + relays, + { + kinds: [Kind.ChannelMessage], + "#e": [channel.id], + }, + { eventFilter }, + ); + const callback = useTimelineCurserIntersectionCallback(timeline); + return ( - - - - - - {metadata?.name} - - - - - - + + + + + + + + {metadata?.name} + + + + + + - + + + + - {drawer.isOpen && } - + + + {drawer.isOpen && } + + ); } diff --git a/src/views/channels/components/channel-chat-log.tsx b/src/views/channels/components/channel-chat-log.tsx deleted file mode 100644 index 34f1e132a..000000000 --- a/src/views/channels/components/channel-chat-log.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useCallback } from "react"; -import { Flex, FlexProps } from "@chakra-ui/react"; -import { Kind } from "nostr-tools"; - -import { NostrEvent } from "../../../types/nostr-event"; -import useTimelineLoader from "../../../hooks/use-timeline-loader"; -import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; -import IntersectionObserverProvider from "../../../providers/intersection-observer"; -import useSubject from "../../../hooks/use-subject"; -import ChannelChatMessage from "./channel-chat-message"; -import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter"; -import { LightboxProvider } from "../../../components/lightbox-provider"; - -export default function ChannelChatLog({ - channel, - relays, - ...props -}: Omit & { channel: NostrEvent; relays: string[] }) { - const clientMuteFilter = useClientSideMuteFilter(); - const eventFilter = useCallback( - (e: NostrEvent) => { - if (clientMuteFilter(e)) return false; - return true; - }, - [clientMuteFilter], - ); - const timeline = useTimelineLoader( - `${channel.id}-chat-messages`, - relays, - { - kinds: [Kind.ChannelMessage], - "#e": [channel.id], - }, - { eventFilter }, - ); - - const messages = useSubject(timeline.timeline); - const callback = useTimelineCurserIntersectionCallback(timeline); - - return ( - - - - {messages.map((message) => ( - - ))} - - - - ); -} diff --git a/src/views/channels/components/channel-chat-message.tsx b/src/views/channels/components/channel-chat-message.tsx deleted file mode 100644 index 625ec26b7..000000000 --- a/src/views/channels/components/channel-chat-message.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Box, Text } from "@chakra-ui/react"; -import { NostrEvent } from "../../../types/nostr-event"; -import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; -import { TrustProvider } from "../../../providers/trust"; -import UserAvatar from "../../../components/user-avatar"; -import UserLink from "../../../components/user-link"; -import { memo, useMemo, useRef } from "react"; -import { EmbedableContent, embedUrls } from "../../../helpers/embeds"; -import { - embedEmoji, - embedNostrHashtags, - embedNostrLinks, - embedNostrMentions, - renderGenericUrl, - renderImageUrl, - renderSoundCloudUrl, - renderStemstrUrl, - renderWavlakeUrl, -} from "../../../components/embed-types"; -import NoteZapButton from "../../../components/note/note-zap-button"; -import Timestamp from "../../../components/timestamp"; - -const ChatMessageContent = memo(({ message }: { message: NostrEvent }) => { - const content = useMemo(() => { - let c: EmbedableContent = [message.content]; - - c = embedUrls(c, [renderImageUrl, renderWavlakeUrl, renderStemstrUrl, renderSoundCloudUrl, renderGenericUrl]); - - // nostr - c = embedNostrLinks(c); - c = embedNostrMentions(c, message); - c = embedNostrHashtags(c, message); - c = embedEmoji(c, message); - - return c; - }, [message.content]); - - return <>{content}; -}); - -function ChannelChatMessage({ message, channel }: { message: NostrEvent; channel: NostrEvent }) { - const ref = useRef(null); - useRegisterIntersectionEntity(ref, message.id); - - return ( - - - - - - - {": "} - - - - - - - - ); -} - -export default memo(ChannelChatMessage); diff --git a/src/views/channels/components/channel-message-block.tsx b/src/views/channels/components/channel-message-block.tsx new file mode 100644 index 000000000..60d15e31b --- /dev/null +++ b/src/views/channels/components/channel-message-block.tsx @@ -0,0 +1,20 @@ +import { ReactNode, memo, useCallback } from "react"; + +import { NostrEvent } from "../../../types/nostr-event"; +import MessageBlock, { MessageBlockProps } from "../../../components/message-block"; +import ChannelMessageContent from "./channel-message-content"; + +function ChannelMessageBlock({ ...props }: Omit) { + const renderContent = useCallback( + (message: NostrEvent, buttons: ReactNode | null) => ( + + {buttons} + + ), + [], + ); + + return ; +} + +export default memo(ChannelMessageBlock); diff --git a/src/views/channels/components/channel-message-content.tsx b/src/views/channels/components/channel-message-content.tsx new file mode 100644 index 000000000..07636c5d1 --- /dev/null +++ b/src/views/channels/components/channel-message-content.tsx @@ -0,0 +1,116 @@ +import { memo, useMemo } from "react"; +import { Box, BoxProps } from "@chakra-ui/react"; + +import { NostrEvent } from "../../../types/nostr-event"; +import { TrustProvider } from "../../../providers/trust"; +import { EmbedableContent, embedUrls } from "../../../helpers/embeds"; +import { + embedCashuTokens, + embedEmoji, + embedImageGallery, + embedLightningInvoice, + embedNostrHashtags, + embedNostrLinks, + embedNostrMentions, + renderAppleMusicUrl, + renderGenericUrl, + renderImageUrl, + renderRedditUrl, + renderSimpleXLink, + renderSongDotLinkUrl, + renderSoundCloudUrl, + renderSpotifyUrl, + renderStemstrUrl, + renderTidalUrl, + renderTwitterUrl, + renderVideoUrl, + renderWavlakeUrl, + renderYoutubeUrl, +} from "../../../components/embed-types"; +import { LightboxProvider } from "../../../components/lightbox-provider"; + +const ChannelMessageContent = memo(({ message, children, ...props }: BoxProps & { message: NostrEvent }) => { + const content = useMemo(() => { + let c: EmbedableContent = [message.content]; + + // image gallery + c = embedImageGallery(c, message); + + // common + c = embedUrls(c, [ + renderSimpleXLink, + renderYoutubeUrl, + renderTwitterUrl, + renderRedditUrl, + renderWavlakeUrl, + renderAppleMusicUrl, + renderSpotifyUrl, + renderTidalUrl, + renderSongDotLinkUrl, + renderStemstrUrl, + renderSoundCloudUrl, + renderImageUrl, + renderVideoUrl, + renderGenericUrl, + ]); + + // bitcoin + c = embedLightningInvoice(c); + + // cashu + c = embedCashuTokens(c); + + // nostr + c = embedNostrLinks(c); + c = embedNostrMentions(c, message); + c = embedNostrHashtags(c, message); + c = embedEmoji(c, message); + + return c; + }, [message.content]); + + return ( + + + + {content} + {children} + + + + ); +}); + +export default ChannelMessageContent; + +// function ChannelChatMessage({ message, channel }: { message: NostrEvent; channel: NostrEvent }) { +// const ref = useRef(null); +// useRegisterIntersectionEntity(ref, message.id); + +// return ( +// +// +// +// +// +// +// {": "} +// +// +// +// +// +// +// +// ); +// } + +// export default memo(ChannelChatMessage); diff --git a/src/views/channels/components/send-message-form.tsx b/src/views/channels/components/send-message-form.tsx new file mode 100644 index 000000000..73b8a3364 --- /dev/null +++ b/src/views/channels/components/send-message-form.tsx @@ -0,0 +1,94 @@ +import { useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import dayjs from "dayjs"; +import { Kind } from "nostr-tools"; + +import { Button, Flex, FlexProps, Heading, useToast } from "@chakra-ui/react"; +import { useSigningContext } from "../../../providers/signing-provider"; +import MagicTextArea, { RefType } from "../../../components/magic-textarea"; +import { useTextAreaUploadFileWithForm } from "../../../hooks/use-textarea-upload-file"; +import clientRelaysService from "../../../services/client-relays"; +import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; +import NostrPublishAction from "../../../classes/nostr-publish-action"; +import { createEmojiTags, ensureNotifyPubkeys, getContentMentions } from "../../../helpers/nostr/post"; +import { useContextEmojis } from "../../../providers/emoji-provider"; + +export default function ChannelMessageForm({ + channel, + rootId, + ...props +}: { channel: NostrEvent; rootId?: string } & Omit) { + const toast = useToast(); + const emojis = useContextEmojis(); + const { requestSignature } = useSigningContext(); + + const [loadingMessage, setLoadingMessage] = useState(""); + const { getValues, setValue, watch, handleSubmit, formState, reset } = useForm({ + defaultValues: { + content: "", + }, + mode: "all", + }); + watch("content"); + + const textAreaRef = useRef(null); + const { onPaste } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue); + + const sendMessage = handleSubmit(async (values) => { + try { + if (!values.content) return; + + let draft: DraftNostrEvent = { + kind: Kind.ChannelMessage, + content: values.content, + tags: [["e", channel.id]], + created_at: dayjs().unix(), + }; + + const contentMentions = getContentMentions(draft.content); + draft = createEmojiTags(draft, emojis); + draft = ensureNotifyPubkeys(draft, contentMentions); + + if (rootId) { + draft.tags.push(["e", rootId, "", "root"]); + } + + setLoadingMessage("Signing..."); + const signed = await requestSignature(draft); + const writeRelays = clientRelaysService.getWriteUrls(); + new NostrPublishAction("Send DM", writeRelays, signed); + reset(); + } catch (e) { + if (e instanceof Error) toast({ status: "error", description: e.message }); + } + setLoadingMessage(""); + }); + + const formRef = useRef(null); + + return ( + + {loadingMessage ? ( + + {loadingMessage} + + ) : ( + <> + setValue("content", e.target.value, { shouldDirty: true })} + rows={2} + isRequired + instanceRef={(inst) => (textAreaRef.current = inst)} + onPaste={onPaste} + onKeyDown={(e) => { + if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit(); + }} + /> + + + )} + + ); +} diff --git a/src/views/dms/chat.tsx b/src/views/dms/chat.tsx index 84e1af1c9..94c30b9ad 100644 --- a/src/views/dms/chat.tsx +++ b/src/views/dms/chat.tsx @@ -1,5 +1,5 @@ import { memo, useCallback, useContext, useEffect, useMemo, useState } from "react"; -import { Button, ButtonGroup, Card, Flex, IconButton, useDisclosure } from "@chakra-ui/react"; +import { Button, ButtonGroup, Card, Flex, IconButton } from "@chakra-ui/react"; import { Kind, nip19 } from "nostr-tools"; import { UNSAFE_DataRouterContext, useLocation, useNavigate, useParams } from "react-router-dom"; @@ -9,7 +9,6 @@ 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 MessageBlock from "./components/message-block"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import useCurrentAccount from "../../hooks/use-current-account"; import { useReadRelayUrls } from "../../hooks/use-client-relays"; @@ -21,9 +20,10 @@ 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 ThreadsProvider from "./components/thread-provider"; +import ThreadsProvider from "../../providers/thread-provider"; import { useRouterMarker } from "../../providers/drawer-sub-view-provider"; import TimelineLoader from "../../classes/timeline-loader"; +import DirectMessageBlock from "./components/direct-message-block"; /** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */ const ChatLog = memo(({ timeline }: { timeline: TimelineLoader }) => { @@ -37,7 +37,7 @@ const ChatLog = memo(({ timeline }: { timeline: TimelineLoader }) => { return ( <> {grouped.map((group) => ( - + ))} ); diff --git a/src/views/dms/components/direct-message-block.tsx b/src/views/dms/components/direct-message-block.tsx new file mode 100644 index 000000000..b779309ae --- /dev/null +++ b/src/views/dms/components/direct-message-block.tsx @@ -0,0 +1,25 @@ +import { ReactNode, memo, useCallback } from "react"; + +import { NostrEvent } from "../../../types/nostr-event"; +import MessageBlock, { MessageBlockProps } from "../../../components/message-block"; +import DecryptPlaceholder from "./decrypt-placeholder"; +import DirectMessageContent from "./direct-message-content"; + +function DirectMessageBlock({ ...props }: Omit) { + const renderContent = useCallback( + (message: NostrEvent, buttons: ReactNode | null) => ( + + {(plaintext) => ( + + {buttons} + + )} + + ), + [], + ); + + return ; +} + +export default memo(DirectMessageBlock); diff --git a/src/views/dms/components/direct-message-content.tsx b/src/views/dms/components/direct-message-content.tsx new file mode 100644 index 000000000..0a07adc21 --- /dev/null +++ b/src/views/dms/components/direct-message-content.tsx @@ -0,0 +1,64 @@ +import { Box, BoxProps } from "@chakra-ui/react"; +import { EmbedableContent, embedUrls } from "../../../helpers/embeds"; +import { NostrEvent } from "../../../types/nostr-event"; +import { + embedCashuTokens, + embedNostrLinks, + renderAppleMusicUrl, + renderGenericUrl, + renderImageUrl, + renderRedditUrl, + renderSimpleXLink, + renderSongDotLinkUrl, + renderSoundCloudUrl, + renderSpotifyUrl, + renderStemstrUrl, + renderTidalUrl, + renderTwitterUrl, + renderVideoUrl, + renderWavlakeUrl, + renderYoutubeUrl, +} from "../../../components/embed-types"; +import { TrustProvider } from "../../../providers/trust"; +import { LightboxProvider } from "../../../components/lightbox-provider"; + +export default function DirectMessageContent({ + event, + text, + children, + ...props +}: { event: NostrEvent; text: string } & BoxProps) { + let content: EmbedableContent = [text]; + + content = embedNostrLinks(content); + content = embedUrls(content, [ + renderSimpleXLink, + renderYoutubeUrl, + renderTwitterUrl, + renderRedditUrl, + renderWavlakeUrl, + renderAppleMusicUrl, + renderSpotifyUrl, + renderTidalUrl, + renderSongDotLinkUrl, + renderStemstrUrl, + renderSoundCloudUrl, + renderImageUrl, + renderVideoUrl, + renderGenericUrl, + ]); + + // cashu + content = embedCashuTokens(content); + + return ( + + + + {content} + {children} + + + + ); +} diff --git a/src/views/dms/components/message-bubble.tsx b/src/views/dms/components/message-bubble.tsx deleted file mode 100644 index 338caec6e..000000000 --- a/src/views/dms/components/message-bubble.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useRef } from "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"; -import { EmbedableContent, embedUrls } from "../../../helpers/embeds"; -import { - embedCashuTokens, - embedNostrLinks, - renderGenericUrl, - renderImageUrl, - renderVideoUrl, -} from "../../../components/embed-types"; -import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; -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 { 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) { - const navigate = useNavigate(); - const location = useLocation(); - - const onClick = () => { - navigate(`.`, { state: { ...location.state, thread: event.id } }); - }; - - return ( - } - 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]; - - content = embedNostrLinks(content); - content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderGenericUrl]); - - // cashu - content = embedCashuTokens(content); - - return ( - - - - {content} - {children} - - - - ); -} - -export type MessageBubbleProps = { message: NostrEvent; showHeader?: boolean; showThreadButton?: boolean } & Omit< - CardProps, - "children" ->; - -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(null); - useRegisterIntersectionEntity(ref, getEventUID(message)); - - const actions = ( - <> - - - {showThreadButton && } - - ); - - return ( - - {showHeader && ( - - - - {actionPosition === "header" && ( - - {actions} - - )} - - )} - - - {(plaintext) => ( - - {!hasReactions && ( - - {actionPosition === "inline" && actions} - - - )} - - )} - - - {hasReactions && ( - - - {actionPosition === "footer" ? actions : } - - - - - )} - - ); -} diff --git a/src/views/dms/components/thread-button.tsx b/src/views/dms/components/thread-button.tsx index ad18e3a2d..7f22f66f3 100644 --- a/src/views/dms/components/thread-button.tsx +++ b/src/views/dms/components/thread-button.tsx @@ -2,7 +2,7 @@ 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 { Thread } from "../../../providers/thread-provider"; import { ChevronRightIcon, ThreadIcon } from "../../../components/icons"; import { IconButtonProps } from "yet-another-react-lightbox"; import { NostrEvent } from "../../../types/nostr-event"; @@ -27,3 +27,25 @@ export default function ThreadButton({ thread }: { thread: Thread }) { ); } + +export function IconThreadButton({ + event, + ...props +}: { event: NostrEvent } & Omit) { + const navigate = useNavigate(); + const location = useLocation(); + + const onClick = () => { + navigate(`.`, { state: { ...location.state, thread: event.id } }); + }; + + return ( + } + onClick={onClick} + aria-label="Reply in thread" + title="Reply in thread" + {...props} + /> + ); +} diff --git a/src/views/dms/components/thread-drawer.tsx b/src/views/dms/components/thread-drawer.tsx index 968c2a118..97d41199c 100644 --- a/src/views/dms/components/thread-drawer.tsx +++ b/src/views/dms/components/thread-drawer.tsx @@ -23,12 +23,12 @@ import UserAvatar from "../../../components/user-avatar"; import UserLink from "../../../components/user-link"; import DecryptPlaceholder from "./decrypt-placeholder"; import Timestamp from "../../../components/timestamp"; -import { Thread, useThreadsContext } from "./thread-provider"; +import { Thread, useThreadsContext } from "../../../providers/thread-provider"; import ThreadButton from "./thread-button"; -import MessageBlock from "./message-block"; import SendMessageForm from "./send-message-form"; import { groupMessages } from "../../../helpers/nostr/dms"; import { useDecryptionContext } from "../../../providers/dycryption-provider"; +import DirectMessageBlock from "./direct-message-block"; function MessagePreview({ message, ...props }: { message: NostrEvent } & Omit) { return ( @@ -82,9 +82,9 @@ function ThreadMessages({ thread, pubkey }: { thread: Thread; pubkey: string }) return ( <> - {thread.root && } + {thread.root && } {grouped.map((group) => ( - + ))} diff --git a/src/views/link/index.tsx b/src/views/link/index.tsx index eba8631d7..38dc91722 100644 --- a/src/views/link/index.tsx +++ b/src/views/link/index.tsx @@ -6,7 +6,6 @@ import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs"; import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../helpers/nostr/lists"; import { ErrorBoundary } from "../../components/error-boundary"; import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; -import { decode } from "ngeohash"; import { TORRENT_KIND } from "../../helpers/nostr/torrents"; function NostrLinkPage() {