feat: render nostr mentions in chat

This commit is contained in:
2023-07-30 23:02:11 +02:00
parent efd2f756fe
commit 7583fa1fd4
8 changed files with 100 additions and 40 deletions

View File

@ -14,7 +14,7 @@ import { EmojiPicker } from "./emoji-picker";
import { Icon } from "./icon"; import { Icon } from "./icon";
import { Emoji } from "./emoji"; import { Emoji } from "./emoji";
import { Profile } from "./profile"; import { Profile } from "./profile";
import { Text } from "./text"; import { Markdown } from "./markdown";
import { SendZapsDialog } from "./send-zap"; import { SendZapsDialog } from "./send-zap";
import { findTag } from "../utils"; import { findTag } from "../utils";
import type { EmojiPack } from "../hooks/emoji"; import type { EmojiPack } from "../hooks/emoji";
@ -151,7 +151,12 @@ export function ChatMessage({
pubkey={ev.pubkey} pubkey={ev.pubkey}
profile={profile} profile={profile}
/> />
<Text tags={ev.tags} content={ev.content} /> <Markdown
element="span"
enableParagraphs={false}
tags={ev.tags}
content={ev.content}
/>
{(hasReactions || hasZaps) && ( {(hasReactions || hasZaps) && (
<div className="message-reactions"> <div className="message-reactions">
{hasZaps && ( {hasZaps && (

View File

@ -1,12 +1,22 @@
.event-container .note { .event-container .note {
max-width: 320px; max-width: 320px;
display: flex;
flex-direction: column;
} }
.event-container .goal { .event-container .goal {
font-size: 14px; font-size: 14px;
} }
.event-container .goal .amount { .event-container .goal .progress-root .amount {
top: -8px; top: -8px;
font-size: 10px; font-size: 10px;
} }
.message .event-container .goal .progress-root .amount {
top: -6px;
}
.message .event-container .note {
max-width: unset;
}

View File

@ -1,7 +1,22 @@
import { Icon } from "element/icon";
export function ExternalIconLink({ size = 32, href, ...rest }) {
return (
<span style={{ cursor: "pointer" }}>
<Icon
name="link"
size={size}
onClick={() => window.open(href, "_blank")}
{...rest}
/>
</span>
);
}
export function ExternalLink({ children, href }) { export function ExternalLink({ children, href }) {
return ( return (
<a href={href} rel="noopener noreferrer" target="_blank"> <a href={href} rel="noopener noreferrer" target="_blank">
{children} {children}
</a> </a>
) );
} }

View File

@ -358,16 +358,11 @@
border-radius: unset; border-radius: unset;
} }
.message .message-container { .message .markdown {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.message .message-container .markdown p {
font-size: 14px; font-size: 14px;
line-height: normal;
} }
.message .message-container .markdown > p { .message .markdown .emoji {
margin: 0; width: unset;
} }

View File

@ -1,7 +1,7 @@
import "./markdown.css"; import "./markdown.css";
import { createElement } from "react";
import { parseNostrLink } from "@snort/system"; import { parseNostrLink } from "@snort/system";
import type { ReactNode } from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
@ -190,11 +190,17 @@ function transformText(ps, tags) {
} }
interface MarkdownProps { interface MarkdownProps {
children: ReactNode; content: string;
tags?: string[]; tags?: string[];
enableParagraphs?: booleam;
} }
export function Markdown({ children, tags = [] }: MarkdownProps) { export function Markdown({
content,
tags = [],
enableParagraphs = true,
element = "div",
}: MarkdownProps) {
const components = useMemo(() => { const components = useMemo(() => {
return { return {
li: ({ children, ...props }) => { li: ({ children, ...props }) => {
@ -202,15 +208,20 @@ export function Markdown({ children, tags = [] }: MarkdownProps) {
}, },
td: ({ children }) => td: ({ children }) =>
children && <td>{transformText(children, tags)}</td>, children && <td>{transformText(children, tags)}</td>,
p: ({ children }) => children && <p>{transformText(children, tags)}</p>, p: ({ children }) =>
enableParagraphs ? (
<p>{transformText(children, tags)}</p>
) : (
transformText(children, tags)
),
a: (props) => { a: (props) => {
return <HyperText link={props.href}>{props.children}</HyperText>; return <HyperText link={props.href}>{props.children}</HyperText>;
}, },
}; };
}, [tags]); }, [tags, enableParagraphs]);
return ( return createElement(
<div className="markdown"> element,
<ReactMarkdown children={children} components={components} /> { className: "markdown" },
</div> <ReactMarkdown components={components}>{content}</ReactMarkdown>,
); );
} }

View File

@ -4,6 +4,11 @@
border-radius: 10px; border-radius: 10px;
} }
.note .note-header {
display: flex;
justify-content: space-between;
}
.note .note-header .profile { .note .note-header .profile {
font-size: 14px; font-size: 14px;
} }

View File

@ -2,6 +2,7 @@ import "./note.css";
import { type NostrEvent } from "@snort/system"; import { type NostrEvent } from "@snort/system";
import { Markdown } from "element/markdown"; import { Markdown } from "element/markdown";
import { ExternalIconLink } from "element/external-link";
import { Profile } from "element/profile"; import { Profile } from "element/profile";
export function Note({ ev }: { ev: NostrEvent }) { export function Note({ ev }: { ev: NostrEvent }) {
@ -9,9 +10,10 @@ export function Note({ ev }: { ev: NostrEvent }) {
<div className="note"> <div className="note">
<div className="note-header"> <div className="note-header">
<Profile avatarClassname="note-avatar" pubkey={ev.pubkey} /> <Profile avatarClassname="note-avatar" pubkey={ev.pubkey} />
<ExternalIconLink size={25} href={`https://snort.social/e/${ev.id}`} />
</div> </div>
<div className="note-content"> <div className="note-content">
<Markdown tags={ev.tags}>{ev.content}</Markdown> <Markdown tags={ev.tags} content={ev.content} />
</div> </div>
</div> </div>
); );

View File

@ -9,7 +9,7 @@ import type { NostrEvent } from "@snort/system";
import { Toggle } from "element/toggle"; import { Toggle } from "element/toggle";
import { useLogin } from "hooks/login"; import { useLogin } from "hooks/login";
import { useUserCards } from "hooks/cards"; import { useCards, useUserCards } from "hooks/cards";
import { CARD, USER_CARDS } from "const"; import { CARD, USER_CARDS } from "const";
import { toTag } from "utils"; import { toTag } from "utils";
import { Login, System } from "index"; import { Login, System } from "index";
@ -55,7 +55,7 @@ const CardPreview = forwardRef(
) : ( ) : (
<img className="card-image" src={image} alt={title} /> <img className="card-image" src={image} alt={title} />
))} ))}
<Markdown children={content} /> <Markdown content={content} />
</div> </div>
); );
}, },
@ -382,12 +382,11 @@ function AddCard({ cards }: AddCardProps) {
); );
} }
export function StreamCards({ host }) { export function StreamCardEditor() {
const login = useLogin(); const login = useLogin();
const canEdit = login?.pubkey === host; const cards = useUserCards(login.pubkey, login.cards.tags, true);
const cards = useUserCards(login.pubkey, login.cards.tags, canEdit);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const components = ( return (
<> <>
<div className="stream-cards"> <div className="stream-cards">
{cards.map((ev) => ( {cards.map((ev) => (
@ -395,17 +394,35 @@ export function StreamCards({ host }) {
))} ))}
{isEditing && <AddCard cards={cards} />} {isEditing && <AddCard cards={cards} />}
</div> </div>
{canEdit && ( <div className="edit-container">
<div className="edit-container"> <Toggle
<Toggle pressed={isEditing}
pressed={isEditing} onPressedChange={setIsEditing}
onPressedChange={setIsEditing} label="Toggle edit mode"
label="Toggle edit mode" text="Edit cards"
text="Edit cards" />
/> </div>
</div>
)}
</> </>
); );
return <DndProvider backend={HTML5Backend}>{components}</DndProvider>; }
export function ReadOnlyStreamCards({ host }) {
const cards = useCards(host);
return (
<div className="stream-cards">
{cards.map((ev) => (
<Card cards={cards} key={ev.id} ev={ev} />
))}
</div>
);
}
export function StreamCards({ host }) {
const login = useLogin();
const canEdit = login?.pubkey === host;
return (
<DndProvider backend={HTML5Backend}>
{canEdit ? <StreamCardEditor /> : <ReadOnlyStreamCards host={host} />}
</DndProvider>
);
} }