feat: collapsible event references in chat
This commit is contained in:
@ -1,33 +1,80 @@
|
||||
import "./event.css";
|
||||
|
||||
import { type NostrLink, EventKind } from "@snort/system";
|
||||
import { useEvent } from "hooks/event";
|
||||
import { GOAL } from "const";
|
||||
import {
|
||||
type NostrLink,
|
||||
type NostrEvent as NostrEventType,
|
||||
EventKind,
|
||||
} from "@snort/system";
|
||||
|
||||
import { Icon } from "element/icon";
|
||||
import { Goal } from "element/goal";
|
||||
import { Note } from "element/note";
|
||||
import { EmojiPack } from "element/emoji-pack";
|
||||
import { Badge } from "element/badge";
|
||||
import { useEvent } from "hooks/event";
|
||||
import { GOAL, EMOJI_PACK } from "const";
|
||||
|
||||
interface EventProps {
|
||||
link: NostrLink;
|
||||
}
|
||||
|
||||
export function Event({ link }: EventProps) {
|
||||
const event = useEvent(link);
|
||||
export function EventIcon({ kind }: { kind: EventKind }) {
|
||||
if (kind === GOAL) {
|
||||
return <Icon name="piggybank" />;
|
||||
}
|
||||
|
||||
if (event?.kind === GOAL) {
|
||||
if (kind === EMOJI_PACK) {
|
||||
return <Icon name="face-content" />;
|
||||
}
|
||||
|
||||
if (kind === EventKind.Badge) {
|
||||
return <Icon name="badge" />;
|
||||
}
|
||||
|
||||
if (kind === EventKind.TextNote) {
|
||||
return <Icon name="note" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function NostrEvent({ ev }: { ev: NostrEventType }) {
|
||||
if (ev?.kind === GOAL) {
|
||||
return (
|
||||
<div className="event-container">
|
||||
<Goal ev={event} />
|
||||
<Goal ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (event?.kind === EventKind.TextNote) {
|
||||
if (ev?.kind === EMOJI_PACK) {
|
||||
return (
|
||||
<div className="event-container">
|
||||
<Note ev={event} />
|
||||
<EmojiPack ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (ev?.kind === EventKind.Badge) {
|
||||
return (
|
||||
<div className="event-container">
|
||||
<Badge ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (ev?.kind === EventKind.TextNote) {
|
||||
return (
|
||||
<div className="event-container">
|
||||
<Note ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Event({ link }: EventProps) {
|
||||
const event = useEvent(link);
|
||||
return event ? <NostrEvent ev={event} /> : null;
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { type NostrLink, EventKind } from "@snort/system";
|
||||
|
||||
import { useEvent } from "hooks/event";
|
||||
import { EMOJI_PACK } from "const";
|
||||
import { EmojiPack } from "element/emoji-pack";
|
||||
import { Badge } from "element/badge";
|
||||
|
||||
interface AddressProps {
|
||||
link: NostrLink;
|
||||
}
|
||||
|
||||
export function Address({ link }: AddressProps) {
|
||||
const event = useEvent(link);
|
||||
|
||||
if (event?.kind === EMOJI_PACK) {
|
||||
return <EmojiPack ev={event} />;
|
||||
}
|
||||
|
||||
if (event?.kind === EventKind.Badge) {
|
||||
return <Badge ev={event} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -14,6 +14,7 @@ import { Emoji as EmojiComponent } from "element/emoji";
|
||||
import { Profile } from "./profile";
|
||||
import { Text } from "element/text";
|
||||
import { SendZapsDialog } from "element/send-zap";
|
||||
import { CollapsibleEvent } from "element/collapsible";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { formatSats } from "number";
|
||||
import { findTag } from "utils";
|
||||
@ -30,6 +31,10 @@ function emojifyReaction(reaction: string) {
|
||||
return reaction;
|
||||
}
|
||||
|
||||
const customComponents = {
|
||||
Event: CollapsibleEvent,
|
||||
};
|
||||
|
||||
export function ChatMessage({
|
||||
streamer,
|
||||
ev,
|
||||
@ -159,7 +164,11 @@ export function ChatMessage({
|
||||
pubkey={ev.pubkey}
|
||||
profile={profile}
|
||||
/>
|
||||
<Text tags={ev.tags} content={ev.content} />
|
||||
<Text
|
||||
tags={ev.tags}
|
||||
content={ev.content}
|
||||
customComponents={customComponents}
|
||||
/>
|
||||
{(hasReactions || hasZaps) && (
|
||||
<div className="message-reactions">
|
||||
{hasZaps && (
|
||||
|
@ -18,3 +18,27 @@
|
||||
color: var(--text-link);
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.collapsed-event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.collapsed-event-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.collapsed-event-header svg {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.expanded-event {
|
||||
}
|
||||
|
@ -1,7 +1,16 @@
|
||||
import "./collapsible.css";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import type { ReactNode } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
|
||||
import type { NostrLink } from "@snort/system";
|
||||
|
||||
import { Mention } from "element/mention";
|
||||
import { NostrEvent, EventIcon } from "element/Event";
|
||||
import { ExternalLink } from "element/external-link";
|
||||
import { useEvent } from "hooks/event";
|
||||
|
||||
interface MediaURLProps {
|
||||
url: URL;
|
||||
@ -30,3 +39,41 @@ export function MediaURL({ url, children }: MediaURLProps) {
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function CollapsibleEvent({ link }: { link: NostrLink }) {
|
||||
const event = useEvent(link);
|
||||
const [open, setOpen] = useState(false);
|
||||
const author = event?.pubkey || link.author;
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
className="collapsible"
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<div className="collapsed-event">
|
||||
<div className="collapsed-event-header">
|
||||
{event && <EventIcon kind={event.kind} />}
|
||||
{author && <Mention pubkey={author} />}
|
||||
</div>
|
||||
<Collapsible.Trigger asChild>
|
||||
<button
|
||||
className={`${
|
||||
open ? "btn btn-small delete-button" : "btn btn-small"
|
||||
}`}
|
||||
>
|
||||
{open ? "Hide" : "Show"}
|
||||
</button>
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Content>
|
||||
{open && event && (
|
||||
<div className="expanded-event">
|
||||
{" "}
|
||||
<NostrEvent ev={event} />
|
||||
</div>
|
||||
)}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
@ -4,12 +4,17 @@ import { useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { HyperText } from "element/hypertext";
|
||||
import { transformText, type Fragment } from "element/text";
|
||||
import {
|
||||
transformText,
|
||||
type Fragment,
|
||||
type NostrComponents,
|
||||
} from "element/text";
|
||||
import type { Tags } from "types";
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
tags?: Tags;
|
||||
customComponents?: NostrComponents;
|
||||
}
|
||||
|
||||
interface LinkProps {
|
||||
@ -21,20 +26,36 @@ interface ComponentProps {
|
||||
children?: Array<Fragment>;
|
||||
}
|
||||
|
||||
export function Markdown({ content, tags = [] }: MarkdownProps) {
|
||||
export function Markdown({
|
||||
content,
|
||||
tags = [],
|
||||
customComponents,
|
||||
}: MarkdownProps) {
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
li: ({ children, ...props }: ComponentProps) => {
|
||||
return children && <li {...props}>{transformText(children, tags)}</li>;
|
||||
return (
|
||||
children && (
|
||||
<li {...props}>
|
||||
{transformText(children, tags, customComponents)}
|
||||
</li>
|
||||
)
|
||||
);
|
||||
},
|
||||
td: ({ children }: ComponentProps) => {
|
||||
return children && <td>{transformText(children, tags)}</td>;
|
||||
return (
|
||||
children && <td>{transformText(children, tags, customComponents)}</td>
|
||||
);
|
||||
},
|
||||
th: ({ children }: ComponentProps) => {
|
||||
return children && <th>{transformText(children, tags)}</th>;
|
||||
return (
|
||||
children && <th>{transformText(children, tags, customComponents)}</th>
|
||||
);
|
||||
},
|
||||
p: ({ children }: ComponentProps) => {
|
||||
return children && <p>{transformText(children, tags)}</p>;
|
||||
return (
|
||||
children && <p>{transformText(children, tags, customComponents)}</p>
|
||||
);
|
||||
},
|
||||
a: ({ href, children }: LinkProps) => {
|
||||
return href && <HyperText link={href}>{children}</HyperText>;
|
||||
|
@ -1,9 +1,12 @@
|
||||
import "./text.css";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { useMemo, type ReactNode, type FunctionComponent } from "react";
|
||||
|
||||
import { parseNostrLink, validateNostrLink } from "@snort/system";
|
||||
import {
|
||||
type NostrLink,
|
||||
parseNostrLink,
|
||||
validateNostrLink,
|
||||
} from "@snort/system";
|
||||
|
||||
import { Address } from "element/address";
|
||||
import { Event } from "element/Event";
|
||||
import { Mention } from "element/mention";
|
||||
import { Emoji } from "element/emoji";
|
||||
@ -110,7 +113,7 @@ function extractNpubs(fragments: Fragment[]) {
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractNevents(fragments: Fragment[]) {
|
||||
function extractNevents(fragments: Fragment[], Event: NostrComponent) {
|
||||
return fragments
|
||||
.map((f) => {
|
||||
if (typeof f === "string") {
|
||||
@ -132,7 +135,7 @@ function extractNevents(fragments: Fragment[]) {
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractNaddrs(fragments: Fragment[]) {
|
||||
function extractNaddrs(fragments: Fragment[], Address: NostrComponent) {
|
||||
return fragments
|
||||
.map((f) => {
|
||||
if (typeof f === "string") {
|
||||
@ -155,7 +158,7 @@ function extractNaddrs(fragments: Fragment[]) {
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractNoteIds(fragments: Fragment[]) {
|
||||
function extractNoteIds(fragments: Fragment[], Event: NostrComponent) {
|
||||
return fragments
|
||||
.map((f) => {
|
||||
if (typeof f === "string") {
|
||||
@ -177,12 +180,26 @@ function extractNoteIds(fragments: Fragment[]) {
|
||||
.flat();
|
||||
}
|
||||
|
||||
export function transformText(ps: Fragment[], tags: Array<string[]>) {
|
||||
export type NostrComponent = FunctionComponent<{ link: NostrLink }>;
|
||||
|
||||
export interface NostrComponents {
|
||||
Event: NostrComponent;
|
||||
}
|
||||
|
||||
const components: NostrComponents = {
|
||||
Event,
|
||||
};
|
||||
|
||||
export function transformText(
|
||||
ps: Fragment[],
|
||||
tags: Array<string[]>,
|
||||
customComponents = components
|
||||
) {
|
||||
let fragments = extractEmoji(ps, tags);
|
||||
fragments = extractNprofiles(fragments);
|
||||
fragments = extractNevents(fragments);
|
||||
fragments = extractNaddrs(fragments);
|
||||
fragments = extractNoteIds(fragments);
|
||||
fragments = extractNevents(fragments, customComponents.Event);
|
||||
fragments = extractNaddrs(fragments, customComponents.Event);
|
||||
fragments = extractNoteIds(fragments, customComponents.Event);
|
||||
fragments = extractNpubs(fragments);
|
||||
fragments = extractLinks(fragments);
|
||||
|
||||
@ -192,12 +209,17 @@ export function transformText(ps: Fragment[], tags: Array<string[]>) {
|
||||
interface TextProps {
|
||||
content: string;
|
||||
tags: Tags;
|
||||
customComponents?: NostrComponents;
|
||||
}
|
||||
|
||||
export function Text({ content, tags }: TextProps) {
|
||||
export function Text({ content, tags, customComponents }: TextProps) {
|
||||
// todo: RTL langugage support
|
||||
const element = useMemo(() => {
|
||||
return <span className="text">{transformText([content], tags)}</span>;
|
||||
return (
|
||||
<span className="text">
|
||||
{transformText([content], tags, customComponents)}
|
||||
</span>
|
||||
);
|
||||
}, [content, tags]);
|
||||
|
||||
return <>{element}</>;
|
||||
|
@ -110,6 +110,12 @@ a {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.btn-border {
|
||||
border: 1px solid transparent;
|
||||
color: inherit;
|
||||
|
Reference in New Issue
Block a user