damus.io

damus.io website
git clone git://jb55.com/damus.io
Log | Files | Refs | README | LICENSE

NostrNoteView.tsx (4071B)


      1 import { cn } from "@/lib/utils";
      2 import { useEffect, useMemo, useState } from "react";
      3 import Image from "next/image";
      4 
      5 export interface ParsedNote {
      6   note: Note
      7   parsed_content: ParsedContentBlock[];
      8   profile: Profile;
      9 }
     10 
     11 // This is a note, with the following constraints:
     12 // Kind is always 0
     13 // Content is a JSON string that can be parsed into a ProfileContent
     14 export type Profile = Note;
     15 
     16 export interface ProfileContent {
     17   name: string;
     18   about: string;
     19   deleted: boolean;
     20   display_name: string;
     21   picture: string;  // URL
     22   banner: string;  // URL
     23   nip05: string;  // Email-like address
     24   lud16: string;  // Email-like address
     25   displayName: string;
     26   // There are more fields, but we don't really care about them in this context
     27 }
     28 
     29 export interface ParsedContentBlock {
     30   text?: string;
     31   mention?: string;
     32   hashtag?: string;
     33   url?: string;
     34   indexed_mention?: string;
     35   invoice?: string;
     36 }
     37 
     38 export interface Note {
     39   id: string;
     40   pubkey: string;
     41   created_at: number;
     42   kind: number;
     43   content: string;
     44   tags: string[][];
     45   sig: string;
     46 }
     47 
     48 export interface NostrNoteViewProps {
     49   note: ParsedNote;
     50   className?: string;
     51   style?: React.CSSProperties;
     52 }
     53 
     54 export function NostrNoteView(props: NostrNoteViewProps) {
     55   const profileContent = useMemo(() => {  // JSON parsing is expensive, so we memoize it
     56     if (!props.note?.profile)
     57         return {name: "nostrich", displayName: "nostrich", picture: "https://damus.io/img/no-profile.svg"}
     58     return JSON.parse(props.note.profile.content) as ProfileContent;
     59   }, [props.note?.profile?.content]);
     60   const [timestamp, setTimestamp] = useState<string | null>(null);
     61   const displayName = profileContent.displayName || profileContent.name;
     62 
     63   useEffect(() => {
     64     let created_at = props.note?.note?.created_at || 0;
     65     setTimestamp(new Date(created_at * 1000).toLocaleDateString());
     66   }, [props.note?.note?.created_at]);
     67 
     68   return (
     69     <div className={cn("p-6 bg-white rounded-3xl shadow-lg border border-black/20 text-left", props.className)} style={props.style}>
     70       <div className="flex flex-col gap-y-3">
     71         <div className="flex items-center gap-x-3 text-xl">
     72           <Image
     73             src={profileContent.picture}
     74             className="w-12 h-12 rounded-full"
     75             width={48}
     76             height={48}
     77             alt={displayName}
     78           />
     79           <div className="flex flex-col">
     80             <div className="font-bold text-2xl text-gray-700">
     81               {displayName}
     82             </div>
     83             <div className="text-gray-400 text-sm">
     84               {timestamp}
     85             </div>
     86           </div>
     87         </div>
     88         <div className="text-gray-700 whitespace-pre-wrap break-words">
     89           {props.note?.parsed_content?.map((block, i) => {
     90             return <NoteBlock key={i} block={block} />
     91           }) || null}
     92         </div>
     93       </div>
     94     </div>
     95   )
     96 }
     97 
     98 export function NoteBlock({ block }: { block: ParsedContentBlock }): JSX.Element | null {
     99   if (block.text) {
    100     return <span>{block.text}</span>;
    101   } else if (block.mention) {
    102     return <span className="text-damuspink-600 hover:underline"><a target="_blank" href={mentionLinkAddress(block.mention)}>@{shortenMention(block.mention)}</a></span>;
    103   } else if (block.url) {
    104     return (
    105       /\.(jpg|jpeg|png|gif)$/.test(block.url) ?
    106         <div className="my-2 flex items-center justify-center w-full h-auto max-h-96 overflow-hidden">
    107           <img src={block.url} className="w-full"/>
    108         </div>
    109         :
    110         <a href={block.url} target="_blank" rel="noopener noreferrer">{block.url}</a>
    111     );
    112   } else if (block.hashtag) {
    113     return <span className="text-damuspink-600 hover:underline"><a href={"damus:t:" + block.hashtag}>#{block.hashtag}</a></span>;
    114   } else {
    115     return null;
    116   }
    117 }
    118 
    119 function mentionLinkAddress(mention: string) {
    120   if (mention.startsWith("note")) {
    121     return "https://damus.io/" + mention;
    122   }
    123   else {
    124     return "https://njump.me/" + mention;
    125   }
    126 }
    127 
    128 function shortenMention(npub: string) {
    129   return npub.substring(0, 8) + ":" + npub.substring(npub.length - 8);
    130 }