feat: render nostr mentions in chat

This commit is contained in:
verbiricha 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 { Emoji } from "./emoji";
import { Profile } from "./profile";
import { Text } from "./text";
import { Markdown } from "./markdown";
import { SendZapsDialog } from "./send-zap";
import { findTag } from "../utils";
import type { EmojiPack } from "../hooks/emoji";
@ -151,7 +151,12 @@ export function ChatMessage({
pubkey={ev.pubkey}
profile={profile}
/>
<Text tags={ev.tags} content={ev.content} />
<Markdown
element="span"
enableParagraphs={false}
tags={ev.tags}
content={ev.content}
/>
{(hasReactions || hasZaps) && (
<div className="message-reactions">
{hasZaps && (

View File

@ -1,12 +1,22 @@
.event-container .note {
max-width: 320px;
display: flex;
flex-direction: column;
}
.event-container .goal {
font-size: 14px;
}
.event-container .goal .amount {
.event-container .goal .progress-root .amount {
top: -8px;
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 }) {
return (
<a href={href} rel="noopener noreferrer" target="_blank">
{children}
</a>
)
);
}

View File

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

View File

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

View File

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

View File

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

View File

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