From 59cf6506e03a03d0dd839f46d3849ae9938e581e Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Wed, 26 Jul 2023 16:13:47 +0200 Subject: [PATCH 1/3] feat: render emoji packs and goals in cards --- src/const.ts | 2 + src/element/Address.tsx | 19 ++++ src/element/Event.tsx | 33 ++++++ src/element/address.css | 0 src/element/chat-message.tsx | 20 ++-- src/element/emoji-pack.css | 20 ++++ src/element/emoji-pack.tsx | 24 ++++ src/element/event.css | 8 ++ src/element/goal.tsx | 38 +++++-- src/element/hypertext.tsx | 48 +++++++- src/element/live-chat.css | 8 +- src/element/live-chat.tsx | 29 ++--- src/element/markdown.css | 8 +- src/element/markdown.tsx | 209 ++++++++++++++++++++++++++++++++++- src/element/note.css | 22 ++++ src/element/note.tsx | 18 +++ src/hooks/emoji.tsx | 21 ++-- src/hooks/event.ts | 43 +++++++ src/hooks/goals.ts | 26 ++++- src/index.css | 7 ++ src/pages/profile-page.tsx | 10 +- 21 files changed, 537 insertions(+), 76 deletions(-) create mode 100644 src/element/Address.tsx create mode 100644 src/element/Event.tsx create mode 100644 src/element/address.css create mode 100644 src/element/emoji-pack.css create mode 100644 src/element/emoji-pack.tsx create mode 100644 src/element/event.css create mode 100644 src/element/note.css create mode 100644 src/element/note.tsx create mode 100644 src/hooks/event.ts diff --git a/src/const.ts b/src/const.ts index ca60be4..7930080 100644 --- a/src/const.ts +++ b/src/const.ts @@ -2,6 +2,8 @@ import { EventKind } from "@snort/system"; export const LIVE_STREAM = 30_311 as EventKind; export const LIVE_STREAM_CHAT = 1_311 as EventKind; +export const EMOJI_PACK = 30_030 as EventKind; +export const USER_EMOJIS = 10_030 as EventKind; export const GOAL = 9041 as EventKind; export const USER_CARDS = 17_777 as EventKind; export const CARD = 37_777 as EventKind; diff --git a/src/element/Address.tsx b/src/element/Address.tsx new file mode 100644 index 0000000..05bcb6c --- /dev/null +++ b/src/element/Address.tsx @@ -0,0 +1,19 @@ +import { type NostrLink } from "@snort/system"; + +import { useEvent } from "hooks/event"; +import { EMOJI_PACK } from "const"; +import { EmojiPack } from "element/emoji-pack"; + +interface AddressProps { + link: NostrLink; +} + +export function Address({ link }: AddressProps) { + const event = useEvent(link); + + if (event?.kind === EMOJI_PACK) { + return ; + } + + return null; +} diff --git a/src/element/Event.tsx b/src/element/Event.tsx new file mode 100644 index 0000000..bb43c08 --- /dev/null +++ b/src/element/Event.tsx @@ -0,0 +1,33 @@ +import "./event.css"; + +import { type NostrLink, EventKind } from "@snort/system"; +import { useEvent } from "hooks/event"; +import { GOAL } from "const"; +import { Goal } from "element/goal"; +import { Note } from "element/note"; + +interface EventProps { + link: NostrLink; +} + +export function Event({ link }: EventProps) { + const event = useEvent(link); + + if (event && event.kind === GOAL) { + return ( +
+ +
+ ); + } + + if (event && event.kind === EventKind.TextNote) { + return ( +
+ +
+ ); + } + + return {link.id}; +} diff --git a/src/element/address.css b/src/element/address.css new file mode 100644 index 0000000..e69de29 diff --git a/src/element/chat-message.tsx b/src/element/chat-message.tsx index c0fdc49..9c7801b 100644 --- a/src/element/chat-message.tsx +++ b/src/element/chat-message.tsx @@ -58,7 +58,7 @@ export function ChatMessage({ const login = useLogin(); const profile = useUserProfile( System, - inView?.isIntersecting ? ev.pubkey : undefined + inView?.isIntersecting ? ev.pubkey : undefined, ); const zapTarget = profile?.lud16 ?? profile?.lud06; const zaps = useMemo(() => { @@ -178,16 +178,16 @@ export function ChatMessage({ style={ isTablet ? { - display: showZapDialog || isHovering ? "flex" : "none", - } + display: showZapDialog || isHovering ? "flex" : "none", + } : { - position: "fixed", - top: topOffset ? topOffset - 12 : 0, - left: leftOffset ? leftOffset - 32 : 0, - opacity: showZapDialog || isHovering ? 1 : 0, - pointerEvents: - showZapDialog || isHovering ? "auto" : "none", - } + position: "fixed", + top: topOffset ? topOffset - 12 : 0, + left: leftOffset ? leftOffset - 32 : 0, + opacity: showZapDialog || isHovering ? 1 : 0, + pointerEvents: + showZapDialog || isHovering ? "auto" : "none", + } } > {zapTarget && ( diff --git a/src/element/emoji-pack.css b/src/element/emoji-pack.css new file mode 100644 index 0000000..00663ec --- /dev/null +++ b/src/element/emoji-pack.css @@ -0,0 +1,20 @@ +.emoji-pack-title { + display: flex; + align-items: flex-start; + justify-content: space-between; +} + +.emoji-pack-title a { + font-size: 14px; +} + +.emoji-pack-emojis { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 4px; +} + +.emoji-pack h4 { + margin: 0; +} diff --git a/src/element/emoji-pack.tsx b/src/element/emoji-pack.tsx new file mode 100644 index 0000000..3455463 --- /dev/null +++ b/src/element/emoji-pack.tsx @@ -0,0 +1,24 @@ +import "./emoji-pack.css"; +import { type NostrEvent } from "@snort/system"; + +import { Mention } from "element/mention"; +import { findTag } from "utils"; + +export function EmojiPack({ ev }: { ev: NostrEvent }) { + const name = findTag(ev, "d"); + const emoji = ev.tags.filter((e) => e.at(0) === "emoji"); + return ( +
+
+

{name}

+ +
+
+ {emoji.map((e) => { + const [, name, image] = e; + return {name}; + })} +
+
+ ); +} diff --git a/src/element/event.css b/src/element/event.css new file mode 100644 index 0000000..42f3b97 --- /dev/null +++ b/src/element/event.css @@ -0,0 +1,8 @@ +.event-container .goal { + font-size: 14px; +} + +.event-container .goal .amount { + top: -8px; + font-size: 10px; +} diff --git a/src/element/goal.tsx b/src/element/goal.tsx index 1f816fa..d7f838c 100644 --- a/src/element/goal.tsx +++ b/src/element/goal.tsx @@ -2,19 +2,23 @@ import "./goal.css"; import { useMemo } from "react"; import * as Progress from "@radix-ui/react-progress"; import Confetti from "react-confetti"; -import { ParsedZap, NostrEvent } from "@snort/system"; -import { Icon } from "./icon"; + +import { type NostrEvent } from "@snort/system"; +import { useUserProfile } from "@snort/system-react"; + import { findTag } from "utils"; import { formatSats } from "number"; import usePreviousValue from "hooks/usePreviousValue"; +import { SendZapsDialog } from "element/send-zap"; +import { useZaps } from "hooks/goals"; +import { getName } from "element/profile"; +import { System } from "index"; +import { Icon } from "./icon"; -export function Goal({ - ev, - zaps, -}: { - ev: NostrEvent; - zaps: ParsedZap[]; -}) { +export function Goal({ ev }: { ev: NostrEvent }) { + const profile = useUserProfile(System, ev.pubkey); + const zapTarget = profile?.lud16 ?? profile?.lud06; + const zaps = useZaps(ev, true); const goalAmount = useMemo(() => { const amount = findTag(ev, "amount"); return amount ? Number(amount) / 1000 : null; @@ -34,8 +38,8 @@ export function Goal({ const isFinished = progress >= 100; const previousValue = usePreviousValue(isFinished); - return ( -
+ const goalContent = ( +
{ev.content.length > 0 &&

{ev.content}

}
@@ -61,4 +65,16 @@ export function Goal({ )}
); + + return zapTarget ? ( + + ) : ( + goalContent + ); } diff --git a/src/element/hypertext.tsx b/src/element/hypertext.tsx index f7b5e46..8f378b8 100644 --- a/src/element/hypertext.tsx +++ b/src/element/hypertext.tsx @@ -1,25 +1,63 @@ import { NostrLink } from "./nostr-link"; +const FileExtensionRegex = /\.([\w]+)$/i; + interface HyperTextProps { link: string; } -export function HyperText({ link }: HyperTextProps) { +export function HyperText({ link, children }: HyperTextProps) { try { const url = new URL(link); - if (url.protocol === "nostr:" || url.protocol === "web+nostr:") { + const extension = + FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1; + + if (extension) { + switch (extension) { + case "gif": + case "jpg": + case "jpeg": + case "png": + case "bmp": + case "webp": { + return ( + {url.toString()} + ); + } + case "wav": + case "mp3": + case "ogg": { + return
)} @@ -135,7 +130,7 @@ export function LiveChat({
- {goal && } + {goal && } {login?.pubkey === streamer && }
)} @@ -155,7 +150,7 @@ export function LiveChat({ } case EventKind.ZapReceipt: { const zap = zaps.find( - (b) => b.id === a.id && b.receiver === streamer + (b) => b.id === a.id && b.receiver === streamer, ); if (zap) { return ; @@ -202,7 +197,7 @@ function ChatZap({ zap }: { zap: ParsedZap }) { {formatSats(zap.amount)} sats - {zap.content && ( + {zap.content && (
diff --git a/src/element/markdown.css b/src/element/markdown.css index 83826de..5609eb4 100644 --- a/src/element/markdown.css +++ b/src/element/markdown.css @@ -1,8 +1,8 @@ -.markdown a { +.markdown a { color: var(--text-link); } -.markdown ul, .markdown ol { +.markdown > ul, .markdown > ol { margin: 0; padding: 0 12px; font-size: 18px; @@ -10,7 +10,7 @@ line-height: 29px; } -.markdown p { +.markdown > p { font-size: 18px; font-style: normal; overflow-wrap: break-word; @@ -18,7 +18,7 @@ line-height: 29px; /* 161.111% */ } -.markdown img { +.markdown > img { max-height: 230px; width: 100%; } diff --git a/src/element/markdown.tsx b/src/element/markdown.tsx index 0af282b..0632b97 100644 --- a/src/element/markdown.tsx +++ b/src/element/markdown.tsx @@ -1,11 +1,216 @@ import "./markdown.css"; +import { parseNostrLink } from "@snort/system"; +import type { ReactNode } from "react"; +import { useMemo } from "react"; import ReactMarkdown from "react-markdown"; -export function Markdown({ children }) { +import { Address } from "element/Address"; +import { Event } from "element/Event"; +import { Mention } from "element/mention"; +import { Emoji } from "element/emoji"; +import { HyperText } from "element/hypertext"; + +const MentionRegex = /(#\[\d+\])/gi; +const NostrPrefixRegex = /^nostr:/; +const EmojiRegex = /:([\w-]+):/g; + +function extractEmoji(fragments: Fragment[], tags: string[][]) { + return fragments + .map((f) => { + if (typeof f === "string") { + return f.split(EmojiRegex).map((i) => { + const t = tags.find((a) => a[0] === "emoji" && a[1] === i); + if (t) { + return ; + } else { + return i; + } + }); + } + return f; + }) + .flat(); +} + +function extractMentions(fragments, tags) { + return fragments + .map((f) => { + if (typeof f === "string") { + return f.split(MentionRegex).map((match) => { + const matchTag = match.match(/#\[(\d+)\]/); + if (matchTag && matchTag.length === 2) { + const idx = parseInt(matchTag[1]); + const ref = tags?.find((a, i) => i === idx); + if (ref) { + switch (ref[0]) { + case "p": { + return ; + } + case "a": { + return
; + } + default: + // todo: e and t mentions + return ref[1]; + } + } + return null; + } else { + return match; + } + }); + } + return f; + }) + .flat(); +} + +function extractNprofiles(fragments) { + return fragments + .map((f) => { + if (typeof f === "string") { + return f.split(/(nostr:nprofile1[a-z0-9]+)/g).map((i) => { + if (i.startsWith("nostr:nprofile1")) { + try { + const link = parseNostrLink(i.replace(NostrPrefixRegex, "")); + return ; + } catch (error) { + return i; + } + } else { + return i; + } + }); + } + return f; + }) + .flat(); +} + +function extractNpubs(fragments) { + return fragments + .map((f) => { + if (typeof f === "string") { + return f.split(/(nostr:npub1[a-z0-9]+)/g).map((i) => { + if (i.startsWith("nostr:npub1")) { + try { + const link = parseNostrLink(i.replace(NostrPrefixRegex, "")); + return ; + } catch (error) { + return i; + } + } else { + return i; + } + }); + } + return f; + }) + .flat(); +} + +function extractNevents(fragments) { + return fragments + .map((f) => { + if (typeof f === "string") { + return f.split(/(nostr:nevent1[a-z0-9]+)/g).map((i) => { + if (i.startsWith("nostr:nevent1")) { + try { + const link = parseNostrLink(i.replace(NostrPrefixRegex, "")); + return ; + } catch (error) { + return i; + } + } else { + return i; + } + }); + } + return f; + }) + .flat(); +} + +function extractNaddrs(fragments) { + return fragments + .map((f) => { + if (typeof f === "string") { + return f.split(/(nostr:naddr1[a-z0-9]+)/g).map((i) => { + if (i.startsWith("nostr:naddr1")) { + try { + const link = parseNostrLink(i.replace(NostrPrefixRegex, "")); + return
; + } catch (error) { + console.error(error); + return i; + } + } else { + return i; + } + }); + } + return f; + }) + .flat(); +} + +function extractNoteIds(fragments) { + return fragments + .map((f) => { + if (typeof f === "string") { + return f.split(/(nostr:note1[a-z0-9]+)/g).map((i) => { + if (i.startsWith("nostr:note1")) { + try { + const link = parseNostrLink(i.replace(NostrPrefixRegex, "")); + return ; + } catch (error) { + return i; + } + } else { + return i; + } + }); + } + return f; + }) + .flat(); +} + +function transformText(ps, tags) { + let fragments = extractMentions(ps, tags); + fragments = extractNprofiles(fragments); + fragments = extractNevents(fragments); + fragments = extractNaddrs(fragments); + fragments = extractNoteIds(fragments); + fragments = extractNpubs(fragments); + fragments = extractEmoji(fragments, tags); + + return fragments; +} + +interface MarkdownProps { + children: ReactNode; + tags?: string[]; +} + +export function Markdown({ children, tags = [] }: MarkdownProps) { + const components = useMemo(() => { + return { + li: ({ children, ...props }) => { + return children &&
  • {transformText(children, tags)}
  • ; + }, + td: ({ children }) => + children && {transformText(children, tags)}, + p: ({ children }) => children &&

    {transformText(children, tags)}

    , + a: (props) => { + return {props.children}; + }, + }; + }, [tags]); return (
    - +
    ); } diff --git a/src/element/note.css b/src/element/note.css new file mode 100644 index 0000000..2fb260e --- /dev/null +++ b/src/element/note.css @@ -0,0 +1,22 @@ +.note { + padding: 12px; + border: 1px solid var(--border); + border-radius: 10px; +} + +.note .note-header .profile { + font-size: 14px; +} + +.note .note-avatar { + width: 18px; + height: 18px; +} + +.note .note-content { + margin-left: 30px; +} + +.note .note-content .markdown > p { + font-size: 14px; +} diff --git a/src/element/note.tsx b/src/element/note.tsx new file mode 100644 index 0000000..b79987d --- /dev/null +++ b/src/element/note.tsx @@ -0,0 +1,18 @@ +import "./note.css"; +import { type NostrEvent } from "@snort/system"; + +import { Markdown } from "element/markdown"; +import { Profile } from "element/profile"; + +export function Note({ ev }: { ev: NostrEvent }) { + return ( +
    +
    + +
    +
    + {ev.content} +
    +
    + ); +} diff --git a/src/hooks/emoji.tsx b/src/hooks/emoji.tsx index acfbc3e..54c28df 100644 --- a/src/hooks/emoji.tsx +++ b/src/hooks/emoji.tsx @@ -1,6 +1,5 @@ import { RequestBuilder, - EventKind, ReplaceableNoteStore, NoteCollection, NostrEvent, @@ -9,6 +8,7 @@ import { useRequestBuilder } from "@snort/system-react"; import { System } from "index"; import { useMemo } from "react"; import { findTag } from "utils"; +import { EMOJI_PACK, USER_EMOJIS } from "const"; import type { EmojiTag } from "../element/emoji"; import uniqBy from "lodash.uniqby"; @@ -49,9 +49,7 @@ export default function useEmoji(pubkey?: string) { if (!pubkey) return null; const rb = new RequestBuilder(`emoji:${pubkey}`); - rb.withFilter() - .authors([pubkey]) - .kinds([10030 as EventKind]); + rb.withFilter().authors([pubkey]).kinds([USER_EMOJIS]); return rb; }, [pubkey]); @@ -59,13 +57,13 @@ export default function useEmoji(pubkey?: string) { const { data: userEmoji } = useRequestBuilder( System, ReplaceableNoteStore, - sub + sub, ); const related = useMemo(() => { if (userEmoji) { return userEmoji.tags.filter( - (t) => t.at(0) === "a" && t.at(1)?.startsWith(`30030:`) + (t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`), ); } return []; @@ -85,14 +83,9 @@ export default function useEmoji(pubkey?: string) { const rb = new RequestBuilder(`emoji-related:${pubkey}`); - rb.withFilter() - .kinds([30030 as EventKind]) - .authors(authors) - .tag("d", identifiers); + rb.withFilter().kinds([EMOJI_PACK]).authors(authors).tag("d", identifiers); - rb.withFilter() - .kinds([30030 as EventKind]) - .authors([pubkey]); + rb.withFilter().kinds([EMOJI_PACK]).authors([pubkey]); return rb; }, [pubkey, related]); @@ -100,7 +93,7 @@ export default function useEmoji(pubkey?: string) { const { data: relatedData } = useRequestBuilder( System, NoteCollection, - subRelated + subRelated, ); const emojiPacks = useMemo(() => { diff --git a/src/hooks/event.ts b/src/hooks/event.ts new file mode 100644 index 0000000..302ec3f --- /dev/null +++ b/src/hooks/event.ts @@ -0,0 +1,43 @@ +import { useMemo } from "react"; + +import { + NostrPrefix, + ReplaceableNoteStore, + RequestBuilder, + type NostrLink, +} from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; + +import { System } from "index"; + +export function useEvent(link: NostrLink) { + const sub = useMemo(() => { + const b = new RequestBuilder(`event:${link.id.slice(0, 12)}`); + if (link.type === NostrPrefix.Address) { + const f = b.withFilter().tag("d", [link.id]); + if (link.author) { + f.authors([link.author]); + } + if (link.kind) { + f.kinds([link.kind]); + } + } else { + const f = b.withFilter().ids([link.id]); + if (link.relays) { + link.relays.slice(0, 2).forEach((r) => f.relay(r)); + } + if (link.author) { + f.authors([link.author]); + } + } + return b; + }, [link]); + + const { data } = useRequestBuilder( + System, + ReplaceableNoteStore, + sub, + ); + + return data; +} diff --git a/src/hooks/goals.ts b/src/hooks/goals.ts index 7b7cff1..83560b8 100644 --- a/src/hooks/goals.ts +++ b/src/hooks/goals.ts @@ -1,13 +1,37 @@ import { useMemo } from "react"; import { + EventKind, + NostrEvent, RequestBuilder, + NoteCollection, ReplaceableNoteStore, NostrLink, + parseZap, } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { GOAL } from "const"; import { System } from "index"; +export function useZaps(goal: NostrEvent, leaveOpen = false) { + const sub = useMemo(() => { + const b = new RequestBuilder(`goal-zaps:${goal.id.slice(0, 12)}`); + b.withOptions({ leaveOpen }); + b.withFilter() + .kinds([EventKind.ZapReceipt]) + .tag("e", [goal.id]) + .since(goal.created_at); + return b; + }, [goal, leaveOpen]); + + const { data } = useRequestBuilder( + System, + NoteCollection, + sub, + ); + + return data?.map((ev) => parseZap(ev, System.ProfileLoader.Cache)).filter((z) => z && z.valid) ?? []; +} + export function useZapGoal(host: string, link: NostrLink, leaveOpen = false) { const sub = useMemo(() => { const b = new RequestBuilder(`goals:${host.slice(0, 12)}`); @@ -22,7 +46,7 @@ export function useZapGoal(host: string, link: NostrLink, leaveOpen = false) { const { data } = useRequestBuilder( System, ReplaceableNoteStore, - sub + sub, ); return data; diff --git a/src/index.css b/src/index.css index e500e2d..ac99df4 100644 --- a/src/index.css +++ b/src/index.css @@ -15,6 +15,7 @@ body { --text-muted: #797979; --text-link: #F838D9; --text-danger: #FF563F; + --border: #333; } @media(max-width: 1020px) { @@ -270,3 +271,9 @@ div.paper { .szh-menu__item--hover { background-color: unset; } + +.emoji { + width: 15px; + height: 15px; + margin-bottom: -2px; +} diff --git a/src/pages/profile-page.tsx b/src/pages/profile-page.tsx index 67faffc..34dab78 100644 --- a/src/pages/profile-page.tsx +++ b/src/pages/profile-page.tsx @@ -62,7 +62,7 @@ export function ProfilePage() { }, [streams]); const futureStreams = useMemo(() => { return streams.filter( - (ev) => findTag(ev, "status") === StreamState.Planned + (ev) => findTag(ev, "status") === StreamState.Planned, ); }, [streams]); const isLive = Boolean(liveEvent); @@ -75,7 +75,7 @@ export function ProfilePage() { d, undefined, liveEvent.kind, - liveEvent.pubkey + liveEvent.pubkey, ); navigate(`/${naddr}`); } @@ -114,7 +114,7 @@ export function ProfilePage() { liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag( liveEvent, - "d" + "d", )}` : undefined } @@ -171,7 +171,7 @@ export function ProfilePage() { Streamed on{" "} {moment(Number(ev.created_at) * 1000).format( - "MMM DD, YYYY" + "MMM DD, YYYY", )} @@ -186,7 +186,7 @@ export function ProfilePage() { Scheduled for{" "} {moment(Number(ev.created_at) * 1000).format( - "MMM DD, YYYY h:mm:ss a" + "MMM DD, YYYY h:mm:ss a", )} From a6caf709b12d2d0f9403f94e4920bd5a88b6551b Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Wed, 26 Jul 2023 16:33:12 +0200 Subject: [PATCH 2/3] fix: lower big zap threshold --- src/element/live-chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 4ca49fe..a292015 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -174,7 +174,7 @@ export function LiveChat({ ); } -const BIG_ZAP_THRESHOLD = 100_000; +const BIG_ZAP_THRESHOLD = 50_000; function ChatZap({ zap }: { zap: ParsedZap }) { if (!zap.valid) { From e3d9d8079a5717c6bf8e95da5905437a21b758d7 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Wed, 26 Jul 2023 20:21:09 +0200 Subject: [PATCH 3/3] fix: emoji styles --- src/element/emoji-pack.css | 12 ++++++++++++ src/element/emoji-pack.tsx | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/element/emoji-pack.css b/src/element/emoji-pack.css index 00663ec..463cfbd 100644 --- a/src/element/emoji-pack.css +++ b/src/element/emoji-pack.css @@ -9,12 +9,24 @@ } .emoji-pack-emojis { + margin-top: 12px; display: flex; flex-direction: row; flex-wrap: wrap; gap: 4px; } +.emoji-definition { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; +} + +.emoji-name { + font-size: 10px; +} + .emoji-pack h4 { margin: 0; } diff --git a/src/element/emoji-pack.tsx b/src/element/emoji-pack.tsx index 3455463..59b5139 100644 --- a/src/element/emoji-pack.tsx +++ b/src/element/emoji-pack.tsx @@ -16,7 +16,12 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
    {emoji.map((e) => { const [, name, image] = e; - return {name}; + return ( +
    + {name} + {name} +
    + ); })}