Skip to content

Commit

Permalink
feature: enhance note content parsing (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
micpst authored Feb 3, 2024
1 parent 6b60756 commit b8917e2
Show file tree
Hide file tree
Showing 19 changed files with 633 additions and 255 deletions.
29 changes: 15 additions & 14 deletions app/components/input/image-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import cn from "clsx";
import { ImagesPreview } from "@/app/lib/types/file";
import type { JSX } from "react";
import NextImage from "@/app/components/ui/next-image";

type ImagePreviewProps = {
imagesPreview: ImagesPreview;
};
interface IImagePreviewProps {
urls: string[];
}

type PostImageBorderRadius = Record<number, string[]>;

Expand All @@ -15,40 +15,41 @@ const postImageBorderRadius: Readonly<PostImageBorderRadius> = {
4: ["rounded-tl-2xl", "rounded-tr-2xl", "rounded-bl-2xl", "rounded-br-2xl"],
};

function ImagePreview({ imagesPreview }: ImagePreviewProps): JSX.Element {
const images = imagesPreview.slice(0, 4);
function ImagePreview({ urls }: IImagePreviewProps): JSX.Element {
const images = urls.slice(0, 4);
const previewCount = images.length;

return (
<div
className={cn(
"grid grid-cols-2 grid-rows-2 rounded-2xl mt-2 gap-0.5",
"grid grid-cols-2 grid-rows-2 rounded-2xl mt-2 gap-0.5 w-fit h-fit border-[1px] border-light-border",
previewCount > 1 && "h-[271px]",
)}
>
{images.map(({ id, src, alt }, index) => (
{images.map((url, i) => (
<button
key={id}
key={i}
type="button"
className={cn(
"accent-tab relative transition-shadow",
postImageBorderRadius[previewCount][index],
postImageBorderRadius[previewCount][i],
{
"col-span-2 row-span-2": previewCount === 1,
"row-span-2":
previewCount === 2 || (index === 0 && previewCount === 3),
previewCount === 2 || (i === 0 && previewCount === 3),
},
)}
>
<NextImage
className={cn(previewCount > 1 ? "w-full h-full" : "max-h-[500px]")}
imgClassName={cn(
"relative cursor-pointer object-cover",
postImageBorderRadius[previewCount][index],
postImageBorderRadius[previewCount][i],
previewCount > 1 ? "w-full h-full" : "max-h-[500px]",
)}
src={src}
alt={alt}
useSkeleton
src={url}
alt="Image preview"
/>
</button>
))}
Expand Down
100 changes: 75 additions & 25 deletions app/components/note/note-content.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,118 @@
import { sanitize } from "dompurify";
import { parseReferences } from "nostr-tools";
import DOMPurify from "isomorphic-dompurify";
import * as linkify from "linkifyjs";
import "linkify-plugin-hashtag";
import Link from "next/link";
import { nip19, parseReferences } from "nostr-tools";
import ReactPlayer from "react-player";
import type { Event } from "nostr-tools";
import { parseImages, parseLinks, parseTags } from "@/app/lib/utils/parsers";
import type { JSX } from "react";
import ImagePreview from "@/app/components/input/image-preview";
import { validateImageUrl, validateVideoUrl } from "@/app/lib/utils/validators";

type NoteContentProps = {
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener");
}
});

interface INoteContentProps {
event: Event;
};
expanded?: boolean;
}

function NoteContent({ event }: NoteContentProps): JSX.Element {
function NoteContent({ event, expanded }: INoteContentProps): JSX.Element {
const references = parseReferences(event);
const images = parseImages(event.content);
const links = parseLinks(event.content);
const tags = parseTags(event.content);

const links = linkify
.find(event.content, { truncate: 42 })
.filter((link) => link.type === "url");

const hashtags = linkify
.find(event.content, {
formatHref: {
hashtag: (href) => `/t/${href.substring(1).toLowerCase()}`,
},
})
.filter((link) => link.type === "hashtag");

const images = links.filter((link) => validateImageUrl(link.href));
const videos = links.filter((link) => validateVideoUrl(link.href));

let augmentedContent = event.content;

references.forEach((reference) => {
let { text, profile, event, address } = reference;
const { text, profile, event, address } = reference;
// let augmentedReference = profile
// ? `<strong>@test</strong>`
// : event
// ? `<em>${eventsCache[event.id].content.slice(0, 5)}</em>`
// : address
// ? `<a href="${text}">[link]</a>`
// : text;
augmentedContent.replaceAll(text, "");
augmentedContent = augmentedContent.replaceAll(text, "");
});

images.forEach((image) => {
augmentedContent = augmentedContent.replaceAll(image, "");
augmentedContent = augmentedContent.replaceAll(image.href, "");
});

videos.forEach((video) => {
augmentedContent = augmentedContent.replaceAll(video.href, "");
});

links.forEach((link) => {
augmentedContent = augmentedContent.replaceAll(
link,
`<a class="text-main-accent hover:underline" href=${link}>${link}</a>`,
link.href,
`<a class="text-main-accent hover:underline" href=${link.href} target="_blank">${link.value}</a>`,
);
});

tags.forEach((tag) => {
hashtags.forEach((hashtag) => {
augmentedContent = augmentedContent.replaceAll(
tag,
`<a class="text-main-accent hover:underline" href="#">${tag}</a>`,
hashtag.value,
`<a class="text-main-accent hover:underline" href=${hashtag.href}>${hashtag.value}</a>`,
);
});

augmentedContent = augmentedContent.trim();

const imagesPreview = images.map((image) => ({
id: image,
src: image,
alt: image,
}));
const truncate = augmentedContent.length > 300 && !expanded;

augmentedContent = truncate
? `${augmentedContent.slice(0, 250)}...`
: augmentedContent;

const imagesUrls = images.map((image) => image.href);
const videosUrls = videos.map((video) => video.href);

return (
<>
<p
onClick={(e) => e.stopPropagation()}
className="whitespace-pre-line break-words"
dangerouslySetInnerHTML={{ __html: sanitize(augmentedContent) }}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(augmentedContent),
}}
/>
{truncate && (
<Link
href={`/n/${nip19.noteEncode(event.id)}`}
className="hover:underline w-full py-3 text-main-accent"
>
Show more
</Link>
)}
<div className="mt-1 flex flex-col gap-2">
{imagesPreview.length ? (
<ImagePreview imagesPreview={imagesPreview} />
{images.length ? <ImagePreview urls={imagesUrls} /> : null}
{videos.length ? (
<ReactPlayer
className="video-player"
width=""
height=""
controls
url={videosUrls}
/>
) : null}
</div>
</>
Expand Down
4 changes: 2 additions & 2 deletions app/components/note/note-date.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import cn from "clsx";
import Link from "next/link";
import { formatDate } from "@/app/lib/utils/date";
import type { Note } from "@/app/lib/types/note";

type NoteDateProps = Pick<Note, "createdAt"> & {
type NoteDateProps = {
createdAt: number;
noteLink: string;
viewNote?: boolean;
};
Expand Down
Loading

0 comments on commit b8917e2

Please sign in to comment.