mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-07-08 15:20:02 +02:00
Clean up embedded note component
This commit is contained in:
5
.changeset/poor-pumas-rush.md
Normal file
5
.changeset/poor-pumas-rush.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Clean up embedded note component
|
@ -1,6 +1,7 @@
|
|||||||
import { Button, Card, CardBody, CardHeader, CardProps, Spacer, useDisclosure } from "@chakra-ui/react";
|
import { MouseEventHandler, useCallback } from "react";
|
||||||
|
import { Card, CardProps, Flex, LinkBox, LinkOverlay, Spacer } from "@chakra-ui/react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import { NoteContents } from "../../note/note-contents";
|
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { UserAvatarLink } from "../../user-avatar-link";
|
import { UserAvatarLink } from "../../user-avatar-link";
|
||||||
import { UserLink } from "../../user-link";
|
import { UserLink } from "../../user-link";
|
||||||
@ -10,33 +11,46 @@ import appSettings from "../../../services/settings/app-settings";
|
|||||||
import EventVerificationIcon from "../../event-verification-icon";
|
import EventVerificationIcon from "../../event-verification-icon";
|
||||||
import { TrustProvider } from "../../../providers/trust";
|
import { TrustProvider } from "../../../providers/trust";
|
||||||
import { NoteLink } from "../../note-link";
|
import { NoteLink } from "../../note-link";
|
||||||
import { ArrowDownSIcon, ArrowUpSIcon } from "../../icons";
|
|
||||||
import Timestamp from "../../timestamp";
|
import Timestamp from "../../timestamp";
|
||||||
import OpenInDrawerButton from "../../open-in-drawer-button";
|
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
|
import { InlineNoteContent } from "../../note/inline-note-content";
|
||||||
|
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
|
||||||
|
import styled from "@emotion/styled";
|
||||||
|
|
||||||
|
const HoverLinkOverlay = styled(LinkOverlay)`
|
||||||
|
&:hover:before {
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default function EmbeddedNote({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
export default function EmbeddedNote({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||||
const { showSignatureVerification } = useSubject(appSettings);
|
const { showSignatureVerification } = useSubject(appSettings);
|
||||||
const expand = useDisclosure();
|
const navigate = useNavigateInDrawer();
|
||||||
|
const to = `/n/${getSharableEventAddress(event)}`;
|
||||||
|
|
||||||
|
const handleClick = useCallback<MouseEventHandler>(
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(to);
|
||||||
|
},
|
||||||
|
[navigate, to],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrustProvider event={event}>
|
<TrustProvider event={event}>
|
||||||
<Card {...props}>
|
<Card as={LinkBox} {...props}>
|
||||||
<CardHeader padding="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
|
<Flex p="2" gap="2" alignItems="center">
|
||||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||||
<UserLink pubkey={event.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
|
<UserLink pubkey={event.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
|
||||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||||
<Button size="sm" onClick={expand.onToggle} leftIcon={expand.isOpen ? <ArrowUpSIcon /> : <ArrowDownSIcon />}>
|
<HoverLinkOverlay as={RouterLink} to={to} onClick={handleClick} />
|
||||||
Expand
|
|
||||||
</Button>
|
|
||||||
<OpenInDrawerButton to={`/n/${getSharableEventAddress(event)}`} size="sm" />
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
||||||
<NoteLink noteId={event.id} color="current" whiteSpace="nowrap">
|
<NoteLink noteId={event.id} color="current" whiteSpace="nowrap">
|
||||||
<Timestamp timestamp={event.created_at} />
|
<Timestamp timestamp={event.created_at} />
|
||||||
</NoteLink>
|
</NoteLink>
|
||||||
</CardHeader>
|
</Flex>
|
||||||
<CardBody p="0">{expand.isOpen && <NoteContents px="2" event={event} />}</CardBody>
|
<InlineNoteContent px="2" event={event} maxLength={96} />
|
||||||
</Card>
|
</Card>
|
||||||
</TrustProvider>
|
</TrustProvider>
|
||||||
);
|
);
|
||||||
|
42
src/components/note/inline-note-content.tsx
Normal file
42
src/components/note/inline-note-content.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, BoxProps } from "@chakra-ui/react";
|
||||||
|
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||||
|
import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../helpers/embeds";
|
||||||
|
import { embedNostrLinks, embedNostrMentions, embedNostrHashtags, embedEmoji, renderGenericUrl } from "../embed-types";
|
||||||
|
import { LightboxProvider } from "../lightbox-provider";
|
||||||
|
|
||||||
|
function buildContents(event: NostrEvent | DraftNostrEvent) {
|
||||||
|
let content: EmbedableContent = [event.content.trim().replace(/\n+/g, "\n")];
|
||||||
|
|
||||||
|
// common
|
||||||
|
content = embedUrls(content, [renderGenericUrl]);
|
||||||
|
|
||||||
|
// nostr
|
||||||
|
content = embedNostrLinks(content);
|
||||||
|
content = embedNostrMentions(content, event);
|
||||||
|
content = embedNostrHashtags(content, event);
|
||||||
|
content = embedEmoji(content, event);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NoteContentsProps = {
|
||||||
|
event: NostrEvent | DraftNostrEvent;
|
||||||
|
maxLength?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InlineNoteContent = React.memo(
|
||||||
|
({ event, maxLength, ...props }: NoteContentsProps & Omit<BoxProps, "children">) => {
|
||||||
|
let content = buildContents(event);
|
||||||
|
let truncated = maxLength !== undefined ? truncateEmbedableContent(content, maxLength) : content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LightboxProvider>
|
||||||
|
<Box whiteSpace="pre-wrap" {...props}>
|
||||||
|
{truncated}
|
||||||
|
{truncated !== content ? "..." : null}
|
||||||
|
</Box>
|
||||||
|
</LightboxProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, BoxProps } from "@chakra-ui/react";
|
import { Box, BoxProps } from "@chakra-ui/react";
|
||||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../helpers/embeds";
|
||||||
import {
|
import {
|
||||||
embedLightningInvoice,
|
embedLightningInvoice,
|
||||||
embedNostrLinks,
|
embedNostrLinks,
|
||||||
@ -60,11 +60,16 @@ function buildContents(event: NostrEvent | DraftNostrEvent, simpleLinks = false)
|
|||||||
export type NoteContentsProps = {
|
export type NoteContentsProps = {
|
||||||
event: NostrEvent | DraftNostrEvent;
|
event: NostrEvent | DraftNostrEvent;
|
||||||
noOpenGraphLinks?: boolean;
|
noOpenGraphLinks?: boolean;
|
||||||
|
maxLength?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoteContents = React.memo(
|
export const NoteContents = React.memo(
|
||||||
({ event, noOpenGraphLinks, ...props }: NoteContentsProps & Omit<BoxProps, "children">) => {
|
({ event, noOpenGraphLinks, maxLength, ...props }: NoteContentsProps & Omit<BoxProps, "children">) => {
|
||||||
const content = buildContents(event, noOpenGraphLinks);
|
let content = buildContents(event, noOpenGraphLinks);
|
||||||
|
|
||||||
|
if (maxLength !== undefined) {
|
||||||
|
content = truncateEmbedableContent(content, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LightboxProvider>
|
<LightboxProvider>
|
||||||
|
@ -92,3 +92,34 @@ export function embedUrls(content: EmbedableContent, handlers: LinkEmbedHandler[
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function truncateEmbedableContent(content: EmbedableContent, maxLength = 256) {
|
||||||
|
let length = 0;
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
|
const chunk = content[i];
|
||||||
|
length += typeof chunk === "string" ? chunk.length : 8;
|
||||||
|
|
||||||
|
if (length > maxLength) {
|
||||||
|
if (typeof chunk === "string") {
|
||||||
|
const newContent = i > 0 ? content.slice(0, i) : [];
|
||||||
|
const chunkLength = chunk.length - (length - maxLength);
|
||||||
|
|
||||||
|
// find the nearest newline
|
||||||
|
const newLines = chunk.matchAll(/\n/g);
|
||||||
|
for (const match of newLines) {
|
||||||
|
console.log(match.index, chunkLength, chunk.length);
|
||||||
|
|
||||||
|
if (match.index && match.index > chunkLength) {
|
||||||
|
newContent.push(chunk.slice(0, match.index));
|
||||||
|
return newContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// just cut the string
|
||||||
|
newContent.push(chunk.slice(0, maxLength - length));
|
||||||
|
return newContent;
|
||||||
|
} else return content.slice(0, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user