1
0
Fork 0

feat: collapsible event references in chat

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

View File

@ -7,6 +7,7 @@
"@emoji-mart/react": "^1.1.1",
"@noble/curves": "^1.1.0",
"@noble/hashes": "^1.3.1",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",

View File

@ -73,5 +73,17 @@
fill="currentColor"
/>
</symbol>
<symbol id="badge" viewBox="0 0 24 24" fill="none">
<path d="M8.87625 13.0953L4.70122 7.87653C4.44132 7.55166 4.31138 7.38922 4.21897 7.20834C4.13698 7.04787 4.07706 6.87705 4.04084 6.70052C4 6.50155 4 6.29354 4 5.8775V5.2C4 4.0799 4 3.51984 4.21799 3.09202C4.40973 2.71569 4.71569 2.40973 5.09202 2.21799C5.51984 2 6.0799 2 7.2 2H16.8C17.9201 2 18.4802 2 18.908 2.21799C19.2843 2.40973 19.5903 2.71569 19.782 3.09202C20 3.51984 20 4.0799 20 5.2V5.8775C20 6.29354 20 6.50155 19.9592 6.70052C19.9229 6.87705 19.863 7.04787 19.781 7.20834C19.6886 7.38922 19.5587 7.55166 19.2988 7.87652L15.1238 13.0953M5.00005 3L12.0001 12L19 3M15.5355 13.4645C17.4882 15.4171 17.4882 18.5829 15.5355 20.5355C13.5829 22.4882 10.4171 22.4882 8.46446 20.5355C6.51185 18.5829 6.51185 15.4171 8.46446 13.4645C10.4171 11.5118 13.5829 11.5118 15.5355 13.4645Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="piggybank" viewBox="0 0 24 24" fill="none">
<path d="M4.99993 13C4.99993 9.68629 7.68622 7 10.9999 7M4.99993 13C4.99993 14.6484 5.66466 16.1415 6.74067 17.226C6.84445 17.3305 6.89633 17.3828 6.92696 17.4331C6.95619 17.4811 6.9732 17.5224 6.98625 17.5771C6.99993 17.6343 6.99993 17.6995 6.99993 17.8298V20.2C6.99993 20.48 6.99993 20.62 7.05443 20.727C7.10236 20.8211 7.17885 20.8976 7.27293 20.9455C7.37989 21 7.5199 21 7.79993 21H9.69993C9.97996 21 10.12 21 10.2269 20.9455C10.321 20.8976 10.3975 20.8211 10.4454 20.727C10.4999 20.62 10.4999 20.48 10.4999 20.2V19.8C10.4999 19.52 10.4999 19.38 10.5544 19.273C10.6024 19.1789 10.6789 19.1024 10.7729 19.0545C10.8799 19 11.0199 19 11.2999 19H12.6999C12.98 19 13.12 19 13.2269 19.0545C13.321 19.1024 13.3975 19.1789 13.4454 19.273C13.4999 19.38 13.4999 19.52 13.4999 19.8V20.2C13.4999 20.48 13.4999 20.62 13.5544 20.727C13.6024 20.8211 13.6789 20.8976 13.7729 20.9455C13.8799 21 14.0199 21 14.2999 21H16.2C16.48 21 16.62 21 16.727 20.9455C16.8211 20.8976 16.8976 20.8211 16.9455 20.727C17 20.62 17 20.48 17 20.2V19.2243C17 19.0223 17 18.9212 17.0288 18.8401C17.0563 18.7624 17.0911 18.708 17.15 18.6502C17.2114 18.59 17.3155 18.5417 17.5237 18.445C18.5059 17.989 19.344 17.2751 19.9511 16.3902C20.0579 16.2346 20.1112 16.1568 20.1683 16.1108C20.2228 16.0668 20.2717 16.0411 20.3387 16.021C20.4089 16 20.4922 16 20.6587 16H21.2C21.48 16 21.62 16 21.727 15.9455C21.8211 15.8976 21.8976 15.8211 21.9455 15.727C22 15.62 22 15.48 22 15.2V11.7857C22 11.5192 22 11.3859 21.9505 11.283C21.9013 11.181 21.819 11.0987 21.717 11.0495C21.6141 11 21.4808 11 21.2143 11C21.0213 11 20.9248 11 20.8471 10.9738C20.7633 10.9456 20.7045 10.908 20.6437 10.8438C20.5874 10.7842 20.5413 10.6846 20.4493 10.4855C20.1538 9.84622 19.7492 9.26777 19.2593 8.77404C19.1555 8.66945 19.1036 8.61716 19.073 8.56687C19.0437 8.51889 19.0267 8.47759 19.0137 8.42294C19 8.36567 19 8.30051 19 8.17018V7.06058C19 6.70053 19 6.52051 18.925 6.39951C18.8593 6.29351 18.7564 6.21588 18.6365 6.18184C18.4995 6.14299 18.3264 6.19245 17.9802 6.29136L15.6077 6.96922C15.5673 6.98074 15.5472 6.9865 15.5267 6.99054C15.5085 6.99414 15.4901 6.99671 15.4716 6.99826C15.4508 7 15.4297 7 15.3874 7H10.9999M4.99993 13H4C2.89543 13 2 12.1046 2 11C2 10.2597 2.4022 9.61337 3 9.26756M10.9999 7H14.9646C14.9879 6.8367 15 6.66976 15 6.5C15 4.567 13.433 3 11.5 3C9.567 3 8 4.567 8 6.5C8 6.9172 8.073 7.31736 8.20692 7.68839C9.04114 7.24881 9.99144 7 10.9999 7Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="note" viewBox="0 0 24 24" fill="none" >
<path d="M7 8.5H12M7 12H15M9.68375 18H16.2C17.8802 18 18.7202 18 19.362 17.673C19.9265 17.3854 20.3854 16.9265 20.673 16.362C21 15.7202 21 14.8802 21 13.2V7.8C21 6.11984 21 5.27976 20.673 4.63803C20.3854 4.07354 19.9265 3.6146 19.362 3.32698C18.7202 3 17.8802 3 16.2 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V20.3355C3 20.8684 3 21.1348 3.10923 21.2716C3.20422 21.3906 3.34827 21.4599 3.50054 21.4597C3.67563 21.4595 3.88367 21.2931 4.29976 20.9602L6.68521 19.0518C7.17252 18.662 7.41617 18.4671 7.68749 18.3285C7.9282 18.2055 8.18443 18.1156 8.44921 18.0613C8.74767 18 9.0597 18 9.68375 18Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="face-content" viewBox="0 0 24 24" fill="none">
<path d="M8 14C8 14 9.5 16 12 16C14.5 16 16 14 16 14M17 9.24C16.605 9.725 16.065 10 15.5 10C14.935 10 14.41 9.725 14 9.24M10 9.24C9.605 9.725 9.065 10 8.5 10C7.935 10 7.41 9.725 7 9.24M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 19 KiB

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;

View File

@ -1980,6 +1980,33 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-collapsible@npm:^1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-collapsible@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-controllable-state": 1.0.1
"@radix-ui/react-use-layout-effect": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 26976e4a72a3e0f4b2c62af2898b3e205c3652af46a3b41cda9a43567fe8381d9ef6afb0b29e3214c450b847f4f2099a533cffc5045844ecab290e9fa6114ca9
languageName: node
linkType: hard
"@radix-ui/react-collection@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-collection@npm:1.0.3"
@ -9984,6 +10011,7 @@ __metadata:
"@formatjs/ts-transformer": ^3.13.1
"@noble/curves": ^1.1.0
"@noble/hashes": ^1.3.1
"@radix-ui/react-collapsible": ^1.0.3
"@radix-ui/react-dialog": ^1.0.4
"@radix-ui/react-progress": ^1.0.3
"@radix-ui/react-tabs": ^1.0.4