feat: custom emoji reactions
This commit is contained in:
@ -1,180 +1,222 @@
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { NostrEvent, parseZap, EventPublisher, EventKind } from "@snort/system";
|
import { NostrEvent, parseZap, EventPublisher, EventKind } from "@snort/system";
|
||||||
import { useRef, useState, useMemo } from "react";
|
import { useRef, useState, useMemo } from "react";
|
||||||
import { useMediaQuery, useHover, useOnClickOutside, useIntersectionObserver } from "usehooks-ts";
|
import {
|
||||||
|
useMediaQuery,
|
||||||
|
useHover,
|
||||||
|
useOnClickOutside,
|
||||||
|
useIntersectionObserver,
|
||||||
|
} from "usehooks-ts";
|
||||||
|
|
||||||
import { System } from "../index";
|
import { System } from "../index";
|
||||||
import { formatSats } from "../number";
|
import { formatSats } from "../number";
|
||||||
import { EmojiPicker } from "./emoji-picker";
|
import { EmojiPicker } from "./emoji-picker";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
import { Emoji } from "./emoji";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "./profile";
|
||||||
import { Text } from "./text";
|
import { Text } from "./text";
|
||||||
import { SendZapsDialog } from "./send-zap";
|
import { SendZapsDialog } from "./send-zap";
|
||||||
import { findTag } from "../utils";
|
import { findTag } from "../utils";
|
||||||
|
import type { EmojiPack } from "../hooks/emoji";
|
||||||
|
|
||||||
interface Emoji {
|
interface Emoji {
|
||||||
id: string;
|
id: string;
|
||||||
native?: string;
|
native?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emojifyReaction(reaction: string) {
|
function emojifyReaction(reaction: string) {
|
||||||
if (reaction === "+") {
|
if (reaction === "+") {
|
||||||
return "💜";
|
return "💜";
|
||||||
}
|
}
|
||||||
if (reaction === "-") {
|
if (reaction === "-") {
|
||||||
return "👎";
|
return "👎";
|
||||||
}
|
}
|
||||||
return reaction;
|
return reaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessage({
|
export function ChatMessage({
|
||||||
streamer,
|
streamer,
|
||||||
ev,
|
ev,
|
||||||
reactions,
|
reactions,
|
||||||
|
emojiPacks,
|
||||||
}: {
|
}: {
|
||||||
ev: NostrEvent;
|
ev: NostrEvent;
|
||||||
streamer: string;
|
streamer: string;
|
||||||
reactions: readonly NostrEvent[];
|
reactions: readonly NostrEvent[];
|
||||||
|
emojiPacks: EmojiPack[];
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const inView = useIntersectionObserver(ref, {
|
const inView = useIntersectionObserver(ref, {
|
||||||
freezeOnceVisible: true
|
freezeOnceVisible: true,
|
||||||
})
|
});
|
||||||
const emojiRef = useRef(null);
|
const emojiRef = useRef(null);
|
||||||
const isTablet = useMediaQuery("(max-width: 1020px)");
|
const isTablet = useMediaQuery("(max-width: 1020px)");
|
||||||
const isHovering = useHover(ref);
|
const isHovering = useHover(ref);
|
||||||
const [showZapDialog, setShowZapDialog] = useState(false);
|
const [showZapDialog, setShowZapDialog] = useState(false);
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const profile = useUserProfile(System, inView?.isIntersecting ? ev.pubkey : undefined);
|
const profile = useUserProfile(
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
System,
|
||||||
const zaps = useMemo(() => {
|
inView?.isIntersecting ? ev.pubkey : undefined
|
||||||
return reactions.filter(a => a.kind === EventKind.ZapReceipt)
|
);
|
||||||
.map(a => parseZap(a, System.ProfileLoader.Cache))
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
.filter(a => a && a.valid);
|
const zaps = useMemo(() => {
|
||||||
}, [reactions])
|
return reactions
|
||||||
const emojis = useMemo(() => {
|
.filter((a) => a.kind === EventKind.ZapReceipt)
|
||||||
const emojified = reactions
|
.map((a) => parseZap(a, System.ProfileLoader.Cache))
|
||||||
.filter((e) => e.kind === EventKind.Reaction && findTag(e, "e") === ev.id)
|
.filter((a) => a && a.valid);
|
||||||
.map((ev) => emojifyReaction(ev.content));
|
}, [reactions]);
|
||||||
return [...new Set(emojified)];
|
const emojiReactions = useMemo(() => {
|
||||||
}, [ev, reactions]);
|
const emojified = reactions
|
||||||
|
.filter((e) => e.kind === EventKind.Reaction && findTag(e, "e") === ev.id)
|
||||||
|
.map((ev) => emojifyReaction(ev.content));
|
||||||
|
return [...new Set(emojified)];
|
||||||
|
}, [ev, reactions]);
|
||||||
|
const emojiNames = emojiPacks.map((p) => p.emojis).flat();
|
||||||
|
|
||||||
const hasReactions = emojis.length > 0;
|
const hasReactions = emojiReactions.length > 0;
|
||||||
const totalZaps = useMemo(() => {
|
const totalZaps = useMemo(() => {
|
||||||
const messageZaps = zaps.filter((z) => z.event === ev.id);
|
const messageZaps = zaps.filter((z) => z.event === ev.id);
|
||||||
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
|
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
|
||||||
}, [zaps, ev]);
|
}, [zaps, ev]);
|
||||||
const hasZaps = totalZaps > 0;
|
const hasZaps = totalZaps > 0;
|
||||||
|
|
||||||
useOnClickOutside(ref, () => {
|
useOnClickOutside(ref, () => {
|
||||||
setShowZapDialog(false);
|
setShowZapDialog(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
useOnClickOutside(emojiRef, () => {
|
useOnClickOutside(emojiRef, () => {
|
||||||
setShowEmojiPicker(false);
|
setShowEmojiPicker(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onEmojiSelect(emoji: Emoji) {
|
function getEmojiById(id: string) {
|
||||||
setShowEmojiPicker(false);
|
return emojiNames.find((e) => e.at(1) === id);
|
||||||
setShowZapDialog(false);
|
}
|
||||||
try {
|
|
||||||
const pub = await EventPublisher.nip7();
|
|
||||||
const reply = await pub?.react(ev, emoji.native || "+1");
|
|
||||||
if (reply) {
|
|
||||||
console.debug(reply);
|
|
||||||
System.BroadcastEvent(reply);
|
|
||||||
}
|
|
||||||
} catch (error) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error
|
async function onEmojiSelect(emoji: Emoji) {
|
||||||
const topOffset = ref.current?.getBoundingClientRect().top;
|
setShowEmojiPicker(false);
|
||||||
// @ts-expect-error
|
setShowZapDialog(false);
|
||||||
const leftOffset = ref.current?.getBoundingClientRect().left;
|
let reply = null;
|
||||||
|
try {
|
||||||
|
const pub = await EventPublisher.nip7();
|
||||||
|
if (emoji.native) {
|
||||||
|
reply = await pub?.react(ev, emoji.native || "+1");
|
||||||
|
} else {
|
||||||
|
const e = getEmojiById(emoji.id);
|
||||||
|
if (e) {
|
||||||
|
reply = await pub?.generic((eb) => {
|
||||||
|
return eb
|
||||||
|
.kind(EventKind.Reaction)
|
||||||
|
.content(`:${emoji.id}:`)
|
||||||
|
.tag(["e", ev.id])
|
||||||
|
.tag(["p", ev.pubkey])
|
||||||
|
.tag(["emoji", e.at(1)!, e.at(2)!]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reply) {
|
||||||
|
console.debug(reply);
|
||||||
|
System.BroadcastEvent(reply);
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
function pickEmoji(ev: any) {
|
// @ts-expect-error
|
||||||
ev.stopPropagation();
|
const topOffset = ref.current?.getBoundingClientRect().top;
|
||||||
setShowEmojiPicker(!showEmojiPicker);
|
// @ts-expect-error
|
||||||
}
|
const leftOffset = ref.current?.getBoundingClientRect().left;
|
||||||
|
|
||||||
return (
|
function pickEmoji(ev: any) {
|
||||||
<>
|
ev.stopPropagation();
|
||||||
<div
|
setShowEmojiPicker(!showEmojiPicker);
|
||||||
className={`message${streamer === ev.pubkey ? " streamer" : ""}`}
|
}
|
||||||
ref={ref}
|
|
||||||
onClick={() => setShowZapDialog(true)}
|
return (
|
||||||
>
|
<>
|
||||||
<Profile
|
<div
|
||||||
icon={
|
className={`message${streamer === ev.pubkey ? " streamer" : ""}`}
|
||||||
ev.pubkey === streamer && (
|
ref={ref}
|
||||||
<Icon name="signal" size={16} />
|
onClick={() => setShowZapDialog(true)}
|
||||||
)
|
>
|
||||||
}
|
<Profile
|
||||||
pubkey={ev.pubkey}
|
icon={ev.pubkey === streamer && <Icon name="signal" size={16} />}
|
||||||
profile={profile}
|
pubkey={ev.pubkey}
|
||||||
/>
|
profile={profile}
|
||||||
<Text content={ev.content} tags={ev.tags} />
|
/>
|
||||||
{(hasReactions || hasZaps) && (
|
<Text content={ev.content} tags={ev.tags} />
|
||||||
<div className="message-reactions">
|
{(hasReactions || hasZaps) && (
|
||||||
{hasZaps && (
|
<div className="message-reactions">
|
||||||
<div className="zap-pill">
|
{hasZaps && (
|
||||||
<Icon name="zap-filled" className="zap-pill-icon" />
|
<div className="zap-pill">
|
||||||
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
|
<Icon name="zap-filled" className="zap-pill-icon" />
|
||||||
</div>
|
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
|
||||||
)}
|
</div>
|
||||||
{emojis.map((e) => (
|
|
||||||
<div className="message-reaction-container">
|
|
||||||
<span className="message-reaction">{e}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{ref.current && (
|
|
||||||
<div
|
|
||||||
className="message-zap-container"
|
|
||||||
style={
|
|
||||||
isTablet
|
|
||||||
? {
|
|
||||||
display: showZapDialog || isHovering ? "flex" : "none",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
position: "fixed",
|
|
||||||
top: topOffset - 12,
|
|
||||||
left: leftOffset - 32,
|
|
||||||
opacity: showZapDialog || isHovering ? 1 : 0,
|
|
||||||
pointerEvents:
|
|
||||||
showZapDialog || isHovering ? "auto" : "none",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{zapTarget && (
|
|
||||||
<SendZapsDialog
|
|
||||||
lnurl={zapTarget}
|
|
||||||
eTag={ev.id}
|
|
||||||
pubkey={ev.pubkey}
|
|
||||||
button={
|
|
||||||
<button className="message-zap-button">
|
|
||||||
<Icon name="zap" className="message-zap-button-icon" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
targetName={profile?.name || ev.pubkey}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<button className="message-zap-button" onClick={pickEmoji}>
|
|
||||||
<Icon name="face" className="message-zap-button-icon" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showEmojiPicker && (
|
|
||||||
<EmojiPicker
|
|
||||||
topOffset={topOffset}
|
|
||||||
leftOffset={leftOffset}
|
|
||||||
onEmojiSelect={onEmojiSelect}
|
|
||||||
onClickOutside={() => setShowEmojiPicker(false)}
|
|
||||||
ref={emojiRef}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
{emojiReactions.map((e) => {
|
||||||
);
|
const isCustomEmojiReaction =
|
||||||
|
e.length > 1 && e.startsWith(":") && e.endsWith(":");
|
||||||
|
const emojiName = e.replace(/:/g, "");
|
||||||
|
const emoji = isCustomEmojiReaction && getEmojiById(emojiName);
|
||||||
|
return (
|
||||||
|
<div className="message-reaction-container">
|
||||||
|
{isCustomEmojiReaction && emoji ? (
|
||||||
|
<span className="message-reaction">
|
||||||
|
<Emoji name={emoji.at(1)!} url={emoji.at(2)!} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="message-reaction">{e}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ref.current && (
|
||||||
|
<div
|
||||||
|
className="message-zap-container"
|
||||||
|
style={
|
||||||
|
isTablet
|
||||||
|
? {
|
||||||
|
display: showZapDialog || isHovering ? "flex" : "none",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
position: "fixed",
|
||||||
|
top: topOffset - 12,
|
||||||
|
left: leftOffset - 32,
|
||||||
|
opacity: showZapDialog || isHovering ? 1 : 0,
|
||||||
|
pointerEvents:
|
||||||
|
showZapDialog || isHovering ? "auto" : "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{zapTarget && (
|
||||||
|
<SendZapsDialog
|
||||||
|
lnurl={zapTarget}
|
||||||
|
eTag={ev.id}
|
||||||
|
pubkey={ev.pubkey}
|
||||||
|
button={
|
||||||
|
<button className="message-zap-button">
|
||||||
|
<Icon name="zap" className="message-zap-button-icon" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
targetName={profile?.name || ev.pubkey}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<button className="message-zap-button" onClick={pickEmoji}>
|
||||||
|
<Icon name="face" className="message-zap-button-icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showEmojiPicker && (
|
||||||
|
<EmojiPicker
|
||||||
|
topOffset={topOffset}
|
||||||
|
leftOffset={leftOffset}
|
||||||
|
emojiPacks={emojiPacks}
|
||||||
|
onEmojiSelect={onEmojiSelect}
|
||||||
|
onClickOutside={() => setShowEmojiPicker(false)}
|
||||||
|
ref={emojiRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -347,6 +347,12 @@
|
|||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-reaction .emoji {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
.zap-pill-amount {
|
.zap-pill-amount {
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
|
@ -9,8 +9,10 @@ import {
|
|||||||
encodeTLV,
|
encodeTLV,
|
||||||
} from "@snort/system";
|
} from "@snort/system";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
import uniqBy from "lodash.uniqby";
|
||||||
|
|
||||||
import { System } from "../index";
|
import { System } from "../index";
|
||||||
|
import useEmoji, { packId } from "../hooks/emoji";
|
||||||
import { useLiveChatFeed } from "../hooks/live-chat";
|
import { useLiveChatFeed } from "../hooks/live-chat";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "./profile";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
@ -76,6 +78,13 @@ export function LiveChat({
|
|||||||
return () => System.ProfileLoader.UntrackMetadata(pubkeys);
|
return () => System.ProfileLoader.UntrackMetadata(pubkeys);
|
||||||
}, [feed.zaps]);
|
}, [feed.zaps]);
|
||||||
|
|
||||||
|
const userEmojiPacks = useEmoji(login!.pubkey);
|
||||||
|
const userEmojis = userEmojiPacks.map((pack) => pack.emojis).flat();
|
||||||
|
const channelEmojiPacks = useEmoji(host);
|
||||||
|
const allEmojiPacks = useMemo(() => {
|
||||||
|
return uniqBy(channelEmojiPacks.concat(userEmojiPacks), packId);
|
||||||
|
}, [userEmojiPacks, channelEmojiPacks]);
|
||||||
|
|
||||||
const zaps = feed.zaps
|
const zaps = feed.zaps
|
||||||
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
||||||
.filter((z) => z && z.valid);
|
.filter((z) => z && z.valid);
|
||||||
@ -137,6 +146,7 @@ export function LiveChat({
|
|||||||
case LIVE_STREAM_CHAT: {
|
case LIVE_STREAM_CHAT: {
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
|
emojiPacks={allEmojiPacks}
|
||||||
streamer={streamer}
|
streamer={streamer}
|
||||||
ev={a}
|
ev={a}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
@ -160,7 +170,7 @@ export function LiveChat({
|
|||||||
{(options?.canWrite ?? true) && (
|
{(options?.canWrite ?? true) && (
|
||||||
<div className="write-message">
|
<div className="write-message">
|
||||||
{login ? (
|
{login ? (
|
||||||
<WriteMessage link={link} />
|
<WriteMessage emojiPacks={allEmojiPacks} link={link} />
|
||||||
) : (
|
) : (
|
||||||
<p>Please login to write messages!</p>
|
<p>Please login to write messages!</p>
|
||||||
)}
|
)}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { NostrLink, EventPublisher, EventKind } from "@snort/system";
|
import { NostrLink, EventPublisher, EventKind } from "@snort/system";
|
||||||
import { useRef, useState, useMemo, ChangeEvent } from "react";
|
import { useRef, useState, useMemo, ChangeEvent } from "react";
|
||||||
import uniqBy from "lodash.uniqby";
|
|
||||||
|
|
||||||
import { LIVE_STREAM_CHAT } from "../const";
|
import { LIVE_STREAM_CHAT } from "../const";
|
||||||
import useEmoji, { packId } from "../hooks/emoji";
|
import useEmoji, { packId } from "../hooks/emoji";
|
||||||
@ -10,27 +9,23 @@ import AsyncButton from "./async-button";
|
|||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { Textarea } from "./textarea";
|
import { Textarea } from "./textarea";
|
||||||
import { EmojiPicker } from "./emoji-picker";
|
import { EmojiPicker } from "./emoji-picker";
|
||||||
|
import type { EmojiPack, Emoji } from "../hooks/emoji";
|
||||||
|
|
||||||
interface Emoji {
|
export function WriteMessage({
|
||||||
id: string;
|
link,
|
||||||
native?: string;
|
emojiPacks,
|
||||||
}
|
}: {
|
||||||
|
link: NostrLink;
|
||||||
export function WriteMessage({ link }: { link: NostrLink }) {
|
emojiPacks: EmojiPack[];
|
||||||
|
}) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const emojiRef = useRef(null);
|
const emojiRef = useRef(null);
|
||||||
const [chat, setChat] = useState("");
|
const [chat, setChat] = useState("");
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const userEmojiPacks = useEmoji(login!.pubkey);
|
const emojis = emojiPacks.map((pack) => pack.emojis).flat();
|
||||||
const userEmojis = userEmojiPacks.map((pack) => pack.emojis).flat();
|
|
||||||
const channelEmojiPacks = useEmoji(link.author!);
|
|
||||||
const channelEmojis = channelEmojiPacks.map((pack) => pack.emojis).flat();
|
|
||||||
const emojis = userEmojis.concat(channelEmojis);
|
|
||||||
const names = emojis.map((t) => t.at(1));
|
const names = emojis.map((t) => t.at(1));
|
||||||
const allEmojiPacks = useMemo(() => {
|
|
||||||
return uniqBy(channelEmojiPacks.concat(userEmojiPacks), packId);
|
|
||||||
}, [userEmojiPacks, channelEmojiPacks]);
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const topOffset = ref.current?.getBoundingClientRect().top;
|
const topOffset = ref.current?.getBoundingClientRect().top;
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@ -112,7 +107,7 @@ export function WriteMessage({ link }: { link: NostrLink }) {
|
|||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
topOffset={topOffset}
|
topOffset={topOffset}
|
||||||
leftOffset={leftOffset}
|
leftOffset={leftOffset}
|
||||||
emojiPacks={allEmojiPacks}
|
emojiPacks={emojiPacks}
|
||||||
onEmojiSelect={onEmojiSelect}
|
onEmojiSelect={onEmojiSelect}
|
||||||
onClickOutside={() => setShowEmojiPicker(false)}
|
onClickOutside={() => setShowEmojiPicker(false)}
|
||||||
ref={emojiRef}
|
ref={emojiRef}
|
||||||
|
@ -12,6 +12,11 @@ import { findTag } from "utils";
|
|||||||
import type { EmojiTag } from "../element/emoji";
|
import type { EmojiTag } from "../element/emoji";
|
||||||
import uniqBy from "lodash.uniqby";
|
import uniqBy from "lodash.uniqby";
|
||||||
|
|
||||||
|
export interface Emoji {
|
||||||
|
native?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EmojiPack {
|
export interface EmojiPack {
|
||||||
address: string;
|
address: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -19,13 +24,19 @@ export interface EmojiPack {
|
|||||||
emojis: EmojiTag[];
|
emojis: EmojiTag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanShortcode(shortcode?: string) {
|
||||||
|
return shortcode?.replace(/\s+/, "_");
|
||||||
|
}
|
||||||
|
|
||||||
function toEmojiPack(ev: NostrEvent): EmojiPack {
|
function toEmojiPack(ev: NostrEvent): EmojiPack {
|
||||||
const d = findTag(ev, "d") || "";
|
const d = findTag(ev, "d") || "";
|
||||||
return {
|
return {
|
||||||
address: `${ev.kind}:${ev.pubkey}:${d}`,
|
address: `${ev.kind}:${ev.pubkey}:${d}`,
|
||||||
name: d,
|
name: d,
|
||||||
author: ev.pubkey,
|
author: ev.pubkey,
|
||||||
emojis: ev.tags.filter((t) => t.at(0) === "emoji") as EmojiTag[],
|
emojis: ev.tags
|
||||||
|
.filter((t) => t.at(0) === "emoji")
|
||||||
|
.map((t) => ["emoji", cleanShortcode(t.at(1)), t.at(2)]) as EmojiTag[],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user