forked from Kieran/zap.stream
feat: render nostr mentions in chat
This commit is contained in:
parent
efd2f756fe
commit
7583fa1fd4
@ -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 && (
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>,
|
||||
);
|
||||
}
|
||||
|
@ -4,6 +4,11 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.note .note-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.note .note-header .profile {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user