feat: collapsible event references in chat

This commit is contained in:
2023-08-02 18:59:01 +02:00
parent a11eeef698
commit 08d554aa8f
11 changed files with 246 additions and 53 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 && (

View File

@ -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 {
}

View File

@ -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>
);
}

View File

@ -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>;

View File

@ -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}</>;

View File

@ -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;