feat: render nostr mentions in chat
This commit is contained in:
@ -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 && (
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user