From 3224b03a98853e3545f6c7187a43636b606574d7 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Sun, 2 Jul 2023 18:20:05 +0200 Subject: [PATCH 01/21] fix: don't render link for anon profiles --- src/element/live-chat.tsx | 2 +- src/element/profile.tsx | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index e5b8db9..d1880f7 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -146,7 +146,7 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) {
- {(options?.showAvatar ?? true) && pubkey === "anon" ? ( - - ) : ( - {profile?.name - )} - {(options?.showName ?? true) && ( + {showAvatar && + (pubkey === "anon" ? ( + + ) : ( + {profile?.name + ))} + {showName && ( {options?.overrideName ?? pubkey === "anon" ? "Anon" From a48991c13eb8f46b3c98e69faecfb3adde51e096 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Sun, 2 Jul 2023 19:53:13 +0200 Subject: [PATCH 02/21] feat: message zaps --- package.json | 1 + src/element/live-chat.css | 51 +++++++++++++++ src/element/live-chat.tsx | 128 ++++++++++++++++++++++++++++++-------- src/element/send-zap.tsx | 12 +++- src/hooks/live-chat.tsx | 35 ++++++++++- yarn.lock | 5 ++ 6 files changed, 204 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 09e172d..596007d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "react-intersection-observer": "^9.5.1", "react-router-dom": "^6.13.0", "semantic-sdp": "^3.26.2", + "usehooks-ts": "^2.9.1", "web-vitals": "^2.1.0", "webrtc-adapter": "^8.2.3" }, diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 681e023..57ab234 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -92,6 +92,7 @@ .live-chat .message { word-wrap: break-word; + position: relative; } .live-chat .message .profile { @@ -217,3 +218,53 @@ .zap-content { margin-top: 8px; } + +.zap-pill { + display: flex; + align-items: center; + margin-top: 4px; + padding: 0 4px; + justify-content: center; + gap: 2px; + border-radius: 8px; + background: #434343; + width: fit-content; +} + +.zap-pill-icon { + width: 12px; + height: 12px; + color: #FF8D2B; +} + +.zap-pill-amount { + color: #FFF; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; + text-transform: lowercase; +} + +.message-zap-button { + cursor: pointer; + position: absolute; + left: 12px; + top: -6px; + background: transparent; + border: none; + display: flex; + background: #434343; + border-radius: 8px; + width: 16px; + height: 16px; + justify-content: center; + align-items: center; +} + +.message-zap-button-icon { + color: #FF8D2B; + width: 12px; + height: 12px; + flex-shrink: 0; +} diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index d1880f7..b7f5765 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -10,9 +10,13 @@ import { import { useState, useEffect, + useMemo, + useRef, type KeyboardEvent, type ChangeEvent, + type LegacyRef, } from "react"; +import { useHover } from "usehooks-ts"; import useEmoji from "hooks/emoji"; import { System } from "index"; @@ -23,10 +27,12 @@ import { Icon } from "./icon"; import { Text } from "./text"; import { Textarea } from "./textarea"; import Spinner from "./spinner"; +import { SendZapsDialog } from "./send-zap"; import { useLogin } from "hooks/login"; import { useUserProfile } from "@snort/system-react"; import { formatSats } from "number"; import useTopZappers from "hooks/top-zappers"; +import { LIVE_STREAM_CHAT } from "const"; export interface LiveChatOptions { canWrite?: boolean; @@ -67,13 +73,22 @@ export function LiveChat({ options?: LiveChatOptions; height?: number; }) { - const messages = useLiveChatFeed(link); + const feed = useLiveChatFeed(link); const login = useLogin(); - const events = messages.data ?? []; - const zaps = events + const zaps = feed.zaps .filter((ev) => ev.kind === EventKind.ZapReceipt) .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) .filter((z) => z && z.valid); + const reactions = feed.reactions + .filter((ev) => ev.kind === EventKind.ZapReceipt) + .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) + .filter((z) => z && z.valid); + const events = useMemo(() => { + return [...feed.messages, ...feed.zaps].sort( + (a, b) => b.created_at - a.created_at + ); + }, [feed.messages, feed.zaps]); + return (
{(options?.showHeader ?? true) && ( @@ -85,20 +100,26 @@ export function LiveChat({
)}
- {[...(messages.data ?? [])] - .sort((a, b) => b.created_at - a.created_at) - .map((a) => { - switch (a.kind) { - case 1311: { - return ; - } - case EventKind.ZapReceipt: { - return ; - } + {events.map((a) => { + switch (a.kind) { + case LIVE_STREAM_CHAT: { + return ( + + ); } - return null; - })} - {messages.data === undefined && } + case EventKind.ZapReceipt: { + return ; + } + } + return null; + })} + {feed.messages.length === 0 && }
{(options?.canWrite ?? true) && (
@@ -113,16 +134,72 @@ export function LiveChat({ ); } -function ChatMessage({ ev, link }: { ev: TaggedRawEvent; link: NostrLink }) { +function ChatMessage({ + streamer, + ev, + link, + reactions, +}: { + streamer: string; + ev: TaggedRawEvent; + link: NostrLink; + reactions: ParsedZap[]; +}) { + const ref = useRef(null); + const isHovering = useHover(ref); + const profile = useUserProfile(System, ev.pubkey); + const zapTarget = profile?.lud16 ?? profile?.lud06; + const totalZaps = useMemo(() => { + const messageZaps = reactions.filter((z) => z.event === ev.id); + return messageZaps.reduce((acc, z) => acc + z.amount, 0); + }, [reactions, ev]); return ( -
- - -
+ <> +
+ {zapTarget && ( + + +
+ ) : ( + <> + ) + } + targetName={profile?.name || ev.pubkey} + /> + )} + + + {totalZaps !== 0 && ( +
+ + {formatSats(totalZaps)} +
+ )} +
+ ); } -function ChatZap({ ev }: { ev: TaggedRawEvent }) { +function ChatZap({ streamer, ev }: { streamer: string; ev: TaggedRawEvent }) { const parsed = parseZap(ev, System.ProfileLoader.Cache); useUserProfile(System, parsed.anonZap ? undefined : parsed.sender); @@ -141,7 +218,8 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) { if (!parsed.valid) { return null; } - return ( + + return parsed.receiver === streamer ? (
@@ -158,7 +236,7 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) {
{parsed.content &&
{parsed.content}
}
- ); + ) : null; } function WriteMessage({ link }: { link: NostrLink }) { @@ -184,7 +262,7 @@ function WriteMessage({ link }: { link: NostrLink }) { const emoji = [...emojiNames].map((name) => emojis.find((e) => e.at(1) === name) ); - eb.kind(1311 as EventKind) + eb.kind(LIVE_STREAM_CHAT as EventKind) .content(chat) .tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"]) .processContent(); diff --git a/src/element/send-zap.tsx b/src/element/send-zap.tsx index c89fc0e..27aa427 100644 --- a/src/element/send-zap.tsx +++ b/src/element/send-zap.tsx @@ -13,6 +13,7 @@ interface SendZapsProps { lnurl: string; pubkey?: string; aTag?: string; + eTag?: string; targetName?: string; onFinish: () => void; button?: ReactNode; @@ -22,6 +23,7 @@ function SendZaps({ lnurl, pubkey, aTag, + eTag, targetName, onFinish, }: SendZapsProps) { @@ -57,7 +59,7 @@ function SendZaps({ const amountInSats = isFiat ? Math.floor((amount / UsdRate) * 1e8) : amount; let zap: NostrEvent | undefined; - if (pubkey && aTag) { + if (pubkey) { zap = await pub.zap( amountInSats * 1000, pubkey, @@ -65,7 +67,13 @@ function SendZaps({ undefined, comment, (eb) => { - return eb.tag(["a", aTag]); + if (aTag) { + eb.tag(["a", aTag]); + } + if (eTag) { + eb.tag(["e", eTag]); + } + return eb; } ); } diff --git a/src/hooks/live-chat.tsx b/src/hooks/live-chat.tsx index 01a28fd..52a5e5a 100644 --- a/src/hooks/live-chat.tsx +++ b/src/hooks/live-chat.tsx @@ -22,5 +22,38 @@ export function useLiveChatFeed(link: NostrLink) { return rb; }, [link]); - return useRequestBuilder(System, FlatNoteStore, sub); + const feed = useRequestBuilder(System, FlatNoteStore, sub); + + const messages = useMemo(() => { + return (feed.data ?? []).filter((ev) => ev.kind === LIVE_STREAM_CHAT); + }, [feed.data]); + const zaps = useMemo(() => { + return (feed.data ?? []).filter((ev) => ev.kind === EventKind.ZapReceipt); + }, [feed.data]); + + const etags = useMemo(() => { + return messages.map((e) => e.id); + }, [messages]); + + const esub = useMemo(() => { + if (etags.length === 0) return null; + const rb = new RequestBuilder(`msg-zaps:${link.id}:${link.author}`); + rb.withOptions({ + leaveOpen: true, + }); + rb.withFilter() + .kinds([EventKind.Reaction, EventKind.ZapReceipt]) + .tag("e", etags); + return rb; + }, [etags]); + + const relatedZaps = useRequestBuilder( + System, + FlatNoteStore, + esub + ); + + const reactions = relatedZaps.data ?? []; + + return { messages, zaps, reactions }; } diff --git a/yarn.lock b/yarn.lock index e2af84b..e36d511 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6323,6 +6323,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +usehooks-ts@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.9.1.tgz#953d3284851ffd097432379e271ce046a8180b37" + integrity sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From f40bcb08753007973b2a2d51dbd9ed41e8d317a6 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Sun, 2 Jul 2023 22:53:55 +0200 Subject: [PATCH 03/21] feat: display message reactions --- src/element/live-chat.css | 13 ++++++++++- src/element/live-chat.tsx | 48 ++++++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 57ab234..7f7e993 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -222,7 +222,6 @@ .zap-pill { display: flex; align-items: center; - margin-top: 4px; padding: 0 4px; justify-content: center; gap: 2px; @@ -237,6 +236,18 @@ color: #FF8D2B; } +.message-reactions { + display: flex; + align-items: flex-end; + gap: 4px; + margin-top: 4px; +} + +.message-reaction { + font-size: 16px; + line-height: 18px; +} + .zap-pill-amount { color: #FFF; font-size: 12px; diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index b7f5765..f24e5cd 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -33,6 +33,7 @@ import { useUserProfile } from "@snort/system-react"; import { formatSats } from "number"; import useTopZappers from "hooks/top-zappers"; import { LIVE_STREAM_CHAT } from "const"; +import { findTag } from "utils"; export interface LiveChatOptions { canWrite?: boolean; @@ -79,10 +80,6 @@ export function LiveChat({ .filter((ev) => ev.kind === EventKind.ZapReceipt) .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) .filter((z) => z && z.valid); - const reactions = feed.reactions - .filter((ev) => ev.kind === EventKind.ZapReceipt) - .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) - .filter((z) => z && z.valid); const events = useMemo(() => { return [...feed.messages, ...feed.zaps].sort( (a, b) => b.created_at - a.created_at @@ -109,7 +106,7 @@ export function LiveChat({ ev={a} link={link} key={a.id} - reactions={reactions} + reactions={feed.reactions} /> ); } @@ -134,6 +131,16 @@ export function LiveChat({ ); } +function emojifyReaction(reaction: string) { + if (reaction === "+") { + return "💜"; + } + if (reaction === "-") { + return "👎"; + } + return reaction; +} + function ChatMessage({ streamer, ev, @@ -143,16 +150,28 @@ function ChatMessage({ streamer: string; ev: TaggedRawEvent; link: NostrLink; - reactions: ParsedZap[]; + reactions: readonly TaggedRawEvent[]; }) { const ref = useRef(null); const isHovering = useHover(ref); const profile = useUserProfile(System, ev.pubkey); const zapTarget = profile?.lud16 ?? profile?.lud06; + const zaps = reactions + .filter((ev) => ev.kind === EventKind.ZapReceipt) + .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) + .filter((z) => z && z.valid); + const emojis = useMemo(() => { + 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 hasReactions = emojis.length > 0; const totalZaps = useMemo(() => { - const messageZaps = reactions.filter((z) => z.event === ev.id); + const messageZaps = zaps.filter((z) => z.event === ev.id); return messageZaps.reduce((acc, z) => acc + z.amount, 0); }, [reactions, ev]); + const hasZaps = totalZaps > 0; return ( <>
- {totalZaps !== 0 && ( -
- - {formatSats(totalZaps)} + {(hasReactions || hasZaps) && ( +
+ {hasZaps && ( +
+ + {formatSats(totalZaps)} +
+ )} + {emojis.map((e) => ( + {e} + ))}
)}
From 2e5ab68c856395973b43bd3741f05a2a2a267cce Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Sun, 2 Jul 2023 22:58:29 +0200 Subject: [PATCH 04/21] refactor: better names --- src/hooks/live-chat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/live-chat.tsx b/src/hooks/live-chat.tsx index 52a5e5a..1fcb92f 100644 --- a/src/hooks/live-chat.tsx +++ b/src/hooks/live-chat.tsx @@ -37,7 +37,7 @@ export function useLiveChatFeed(link: NostrLink) { const esub = useMemo(() => { if (etags.length === 0) return null; - const rb = new RequestBuilder(`msg-zaps:${link.id}:${link.author}`); + const rb = new RequestBuilder(`reactions:${link.id}:${link.author}`); rb.withOptions({ leaveOpen: true, }); @@ -47,13 +47,13 @@ export function useLiveChatFeed(link: NostrLink) { return rb; }, [etags]); - const relatedZaps = useRequestBuilder( + const reactionsSub = useRequestBuilder( System, FlatNoteStore, esub ); - const reactions = relatedZaps.data ?? []; + const reactions = reactionsSub.data ?? []; return { messages, zaps, reactions }; } From 12ae084a9f457dce6943e7f04eaa1111cc593e1b Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 10:08:53 +0200 Subject: [PATCH 05/21] fix: bg color --- src/element/live-chat.css | 14 +++++++++++++- src/element/live-chat.tsx | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 7f7e993..1a982bc 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -226,7 +226,7 @@ justify-content: center; gap: 2px; border-radius: 8px; - background: #434343; + background: rgba(255, 255, 255, 0.10); width: fit-content; } @@ -243,6 +243,18 @@ margin-top: 4px; } +.message-reaction-container { + display: flex; + width: 24px; + height: 24px; + padding: 0px 4px; + justify-content: center; + align-items: center; + gap: 2px; + border-radius: 100px; + background: rgba(255, 255, 255, 0.10); +} + .message-reaction { font-size: 16px; line-height: 18px; diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index f24e5cd..04f34a5 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -216,7 +216,9 @@ function ChatMessage({
)} {emojis.map((e) => ( - {e} +
+ {e} +
))}
)} From 2cdf88339afb539824d9133eb9fd63d0fd3b6212 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 14:02:15 +0200 Subject: [PATCH 06/21] feat: emoji reactions --- package.json | 4 ++ src/element/live-chat.css | 69 +++++++++++------- src/element/live-chat.tsx | 142 ++++++++++++++++++++++++++++++-------- src/icons.svg | 3 + yarn.lock | 20 ++++++ 5 files changed, 185 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 596007d..cc6fc44 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { + "@emoji-mart/data": "^1.1.2", + "@emoji-mart/react": "^1.1.1", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4", "@react-hook/resize-observer": "^1.2.6", @@ -13,6 +15,7 @@ "@types/webscopeio__react-textarea-autocomplete": "^4.7.2", "@webscopeio/react-textarea-autocomplete": "^4.9.2", "buffer": "^6.0.3", + "emoji-mart": "^5.5.2", "hls.js": "^1.4.6", "lodash": "^4.17.21", "moment": "^2.29.4", @@ -21,6 +24,7 @@ "react-dom": "^18.2.0", "react-intersection-observer": "^9.5.1", "react-router-dom": "^6.13.0", + "resize-observer-polyfill": "^1.5.1", "semantic-sdp": "^3.26.2", "usehooks-ts": "^2.9.1", "web-vitals": "^2.1.0", diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 1a982bc..8b814c0 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -192,7 +192,7 @@ position: relative; border-radius: 12px; border: 1px solid transparent; - background: black; + background: #0A0A0A; background-clip: padding-box; padding: 8px 12px; } @@ -236,6 +236,50 @@ color: #FF8D2B; } +.message-zap-container { + display: flex; + padding: 8px; + justify-content: center; + align-items: flex-start; + gap: 12px; + border-radius: 12px; + border: 1px solid #303030; + background: #111; + box-shadow: 0px 7px 4px 0px rgba(0, 0, 0, 0.25); + margin-top: 4px; + width: fit-content; + z-index: 1; + transition: opacity .3s ease-out; +} + +@media (min-width: 1020px) { + .message-zap-container { + flex-direction: column; + } +} + +.message-zap-button { + border: none; + cursor: pointer; + height: 24px; + padding: 0px 4px; + justify-content: center; + align-items: center; + gap: 2px; + border-radius: 100px; + background: rgba(255, 255, 255, 0.05); + color: #FFFFFF66; +} + +.message-zap-button:hover { + color: white; +} + +.message-zap-button-icon { + width: 16px; + height: 16px; +} + .message-reactions { display: flex; align-items: flex-end; @@ -268,26 +312,3 @@ line-height: 18px; text-transform: lowercase; } - -.message-zap-button { - cursor: pointer; - position: absolute; - left: 12px; - top: -6px; - background: transparent; - border: none; - display: flex; - background: #434343; - border-radius: 8px; - width: 16px; - height: 16px; - justify-content: center; - align-items: center; -} - -.message-zap-button-icon { - color: #FF8D2B; - width: 12px; - height: 12px; - flex-shrink: 0; -} diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 04f34a5..ae9b98c 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -16,8 +16,10 @@ import { type ChangeEvent, type LegacyRef, } from "react"; -import { useHover } from "usehooks-ts"; +import { useHover, useOnClickOutside, useMediaQuery } from "usehooks-ts"; +import data from "@emoji-mart/data"; +import Picker from "@emoji-mart/react"; import useEmoji from "hooks/emoji"; import { System } from "index"; import { useLiveChatFeed } from "hooks/live-chat"; @@ -141,6 +143,10 @@ function emojifyReaction(reaction: string) { return reaction; } +interface Emoji { + native: string; +} + function ChatMessage({ streamer, ev, @@ -153,7 +159,10 @@ function ChatMessage({ reactions: readonly TaggedRawEvent[]; }) { const ref = useRef(null); - const isHovering = useHover(ref); + const emojiRef = useRef(null); + const isTablet = useMediaQuery("(max-width: 1020px)"); + const [showZapDialog, setShowZapDialog] = useState(false); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); const profile = useUserProfile(System, ev.pubkey); const zapTarget = profile?.lud16 ?? profile?.lud06; const zaps = reactions @@ -166,45 +175,53 @@ function ChatMessage({ .map((ev) => emojifyReaction(ev.content)); return [...new Set(emojified)]; }, [ev, reactions]); + const hasReactions = emojis.length > 0; const totalZaps = useMemo(() => { const messageZaps = zaps.filter((z) => z.event === ev.id); return messageZaps.reduce((acc, z) => acc + z.amount, 0); }, [reactions, ev]); const hasZaps = totalZaps > 0; + + useOnClickOutside(ref, () => { + setShowZapDialog(false); + }); + + useOnClickOutside(emojiRef, () => { + setShowEmojiPicker(false); + }); + + async function onEmojiSelect(emoji: Emoji) { + setShowEmojiPicker(false); + setShowZapDialog(false); + try { + const pub = await EventPublisher.nip7(); + const reply = await pub?.generic((eb) => { + eb.kind(EventKind.Reaction) + .content(emoji.native) + .tag(["e", ev.id]) + .tag(["p", ev.pubkey]); + return eb; + }); + if (reply) { + console.debug(reply); + System.BroadcastEvent(reply); + } + } catch (error) {} + } + + // @ts-expect-error + const topOffset = ref.current?.getBoundingClientRect().top; + // @ts-expect-error + const leftOffset = ref.current?.getBoundingClientRect().left; + return ( <>
setShowZapDialog(true)} > - {zapTarget && ( - - -
- ) : ( - <> - ) - } - targetName={profile?.name || ev.pubkey} - /> - )} {(hasReactions || hasZaps) && ( @@ -222,7 +239,74 @@ function ChatMessage({ ))} )} + {ref.current && ( +
+ {zapTarget && ( + + + + } + targetName={profile?.name || ev.pubkey} + /> + )} + +
+ )} + {showEmojiPicker && ( +
+ + +
+ )} ); } diff --git a/src/icons.svg b/src/icons.svg index 49be568..d8f8556 100644 --- a/src/icons.svg +++ b/src/icons.svg @@ -21,5 +21,8 @@ + + + diff --git a/yarn.lock b/yarn.lock index e36d511..3300d38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1028,6 +1028,16 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@emoji-mart/data@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.1.2.tgz#777c976f8f143df47cbb23a7077c9ca9fe5fc513" + integrity sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg== + +"@emoji-mart/react@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@emoji-mart/react/-/react-1.1.1.tgz#ddad52f93a25baf31c5383c3e7e4c6e05554312a" + integrity sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -3127,6 +3137,11 @@ electron-to-chromium@^1.4.431: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.433.tgz#305ef5f8ea5fe65d252aae4b0e1088f9e4842533" integrity sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ== +emoji-mart@^5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af" + integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A== + emoji-regex@10.2.1, emoji-regex@^10.2.1: version "10.2.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.2.1.tgz#a41c330d957191efd3d9dfe6e1e8e1e9ab048b3f" @@ -5545,6 +5560,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" From 0a241d7cda736ff6badc88a5c5a09bb81a52fe57 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 14:03:54 +0200 Subject: [PATCH 07/21] fix: remove unused dep --- package.json | 1 - yarn.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/package.json b/package.json index cc6fc44..4451c07 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "react-dom": "^18.2.0", "react-intersection-observer": "^9.5.1", "react-router-dom": "^6.13.0", - "resize-observer-polyfill": "^1.5.1", "semantic-sdp": "^3.26.2", "usehooks-ts": "^2.9.1", "web-vitals": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 3300d38..7ec5ae6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5560,11 +5560,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -resize-observer-polyfill@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" From 59e664a9bf0c0c25e207b8ed927c422eb12287e7 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 14:07:57 +0200 Subject: [PATCH 08/21] fix: reaction styles --- src/element/live-chat.css | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 8b814c0..7e0902c 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -220,14 +220,14 @@ } .zap-pill { - display: flex; - align-items: center; - padding: 0 4px; - justify-content: center; - gap: 2px; - border-radius: 8px; + border-radius: 100px; background: rgba(255, 255, 255, 0.10); width: fit-content; + display: flex; + height: 24px; + padding: 0px 4px; + align-items: center; + gap: 2px; } .zap-pill-icon { @@ -300,15 +300,18 @@ } .message-reaction { - font-size: 16px; - line-height: 18px; + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: 22px; } .zap-pill-amount { + text-transform: lowercase; color: #FFF; - font-size: 12px; + font-size: 15px; + font-family: Outfit; font-style: normal; font-weight: 500; - line-height: 18px; - text-transform: lowercase; + line-height: 22px; } From f1a705f6c9f14649df7add46e7b250a7c25ca1cd Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 14:08:19 +0200 Subject: [PATCH 09/21] fix: reaction styles --- src/element/live-chat.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 7e0902c..6faea88 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -309,9 +309,9 @@ .zap-pill-amount { text-transform: lowercase; color: #FFF; - font-size: 15px; + font-size: 12px; font-family: Outfit; font-style: normal; font-weight: 500; - line-height: 22px; + line-height: 18px; } From 425730d12ba1ecdcb205f63bbb59943cac66195e Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 14:59:29 +0200 Subject: [PATCH 10/21] fix: tags alignment --- src/element/tags.tsx | 10 +++++++++- src/pages/stream-page.tsx | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/element/tags.tsx b/src/element/tags.tsx index 166135c..7bef3d0 100644 --- a/src/element/tags.tsx +++ b/src/element/tags.tsx @@ -1,13 +1,21 @@ +import type { ReactNode } from "react"; import moment from "moment"; import { TaggedRawEvent } from "@snort/system"; import { StreamState } from "index"; import { findTag } from "utils"; -export function Tags({ ev }: { ev: TaggedRawEvent }) { +export function Tags({ + children, + ev, +}: { + children?: ReactNode; + ev: TaggedRawEvent; +}) { const status = findTag(ev, "status"); const start = findTag(ev, "starts"); return (
+ {children} {status === StreamState.Planned && ( {status === StreamState.Planned ? "Starts " : ""} diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx index 0e5f70b..848d212 100644 --- a/src/pages/stream-page.tsx +++ b/src/pages/stream-page.tsx @@ -46,8 +46,11 @@ function ProfileInfo({ link }: { link: NostrLink }) {

{findTag(thisEvent.data, "title")}

{findTag(thisEvent.data, "summary")}

- - {thisEvent?.data && } + {thisEvent?.data && ( + + + + )} {isMine && (
{thisEvent.data && ( From 01fa3da793ba6b0cd3246e9f7fd8b353daa47bbf Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 16:28:26 +0200 Subject: [PATCH 11/21] fix: dont process events if not visible --- src/element/live-chat.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index ae9b98c..ab180c6 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -252,6 +252,7 @@ function ChatMessage({ top: topOffset - 12, left: leftOffset - 32, opacity: showZapDialog ? 1 : 0, + pointerEvents: showZapDialog ? "auto" : "none", } } > From a023ba93bf209dabb37181dfe1a34f0d231f7c68 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 16:30:09 +0200 Subject: [PATCH 12/21] fix: overlay z-index --- src/pages/layout.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/layout.css b/src/pages/layout.css index 2bf8847..5d9c12c 100644 --- a/src/pages/layout.css +++ b/src/pages/layout.css @@ -161,9 +161,11 @@ button span.hide-on-mobile { background-color: rgba(0, 0, 0, 0.8); position: fixed; inset: 0; + z-index: 1; } .dialog-content { + z-index: 2; background-color: #171717; border-radius: 6px; position: fixed; From 9f5875f175bf413417a2085bd70b623f7e79f72b Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Mon, 3 Jul 2023 18:27:44 +0200 Subject: [PATCH 13/21] feat: custom emoji autocomplete --- src/element/live-chat.css | 25 +++++- src/element/live-chat.tsx | 155 ++++++++++++++++++++++++++++++-------- src/hooks/emoji.tsx | 53 ++++++++----- 3 files changed, 181 insertions(+), 52 deletions(-) diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 6faea88..1481a6f 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -262,7 +262,7 @@ border: none; cursor: pointer; height: 24px; - padding: 0px 4px; + padding: 4px; justify-content: center; align-items: center; gap: 2px; @@ -315,3 +315,26 @@ font-weight: 500; line-height: 18px; } + +.message-composer { + display: flex; + flex-direction: column; +} + +.write-message-container { + display: flex; + align-items: center; + gap: 8px; +} + +.write-message-container .paper { + flex: 1; +} + +.write-emoji-button { + color: #FFFFFF80; + cursor: pointer; +} +.write-emoji-button:hover { + color: white; +} diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index ab180c6..2adf90d 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -14,13 +14,13 @@ import { useRef, type KeyboardEvent, type ChangeEvent, - type LegacyRef, + type RefObject, } from "react"; import { useHover, useOnClickOutside, useMediaQuery } from "usehooks-ts"; import data from "@emoji-mart/data"; import Picker from "@emoji-mart/react"; -import useEmoji from "hooks/emoji"; +import useEmoji, { type EmojiPack } from "hooks/emoji"; import { System } from "index"; import { useLiveChatFeed } from "hooks/live-chat"; import AsyncButton from "./async-button"; @@ -37,6 +37,71 @@ import useTopZappers from "hooks/top-zappers"; import { LIVE_STREAM_CHAT } from "const"; import { findTag } from "utils"; +interface EmojiPickerProps { + topOffset: number; + leftOffset: number; + emojiPacks?: EmojiPack[]; + onEmojiSelect: (e: Emoji) => void; + onClickOutside: () => void; + height?: number; + ref: RefObject; +} + +function EmojiPicker({ + topOffset, + leftOffset, + onEmojiSelect, + onClickOutside, + emojiPacks = [], + height = 300, + ref, +}: EmojiPickerProps) { + const customEmojiList = emojiPacks.map((pack) => { + return { + id: pack.address, + name: pack.name, + emojis: pack.emojis.map((e) => { + const [, name, url] = e; + return { + id: name, + name, + skins: [{ src: url }], + }; + }), + }; + }); + return ( + <> +
+ + +
+ + ); +} + export interface LiveChatOptions { canWrite?: boolean; showHeader?: boolean; @@ -144,7 +209,8 @@ function emojifyReaction(reaction: string) { } interface Emoji { - native: string; + id: string; + native?: string; } function ChatMessage({ @@ -198,7 +264,7 @@ function ChatMessage({ const pub = await EventPublisher.nip7(); const reply = await pub?.generic((eb) => { eb.kind(EventKind.Reaction) - .content(emoji.native) + .content(emoji.native || "+1") .tag(["e", ev.id]) .tag(["p", ev.pubkey]); return eb; @@ -215,6 +281,11 @@ function ChatMessage({ // @ts-expect-error const leftOffset = ref.current?.getBoundingClientRect().left; + function pickEmoji(ev: any) { + ev.stopPropagation(); + setShowEmojiPicker(!showEmojiPicker); + } + return ( <>
)} -
)}
{showEmojiPicker && ( -
setShowEmojiPicker(false)} ref={emojiRef} - > - - -
+ /> )} ); @@ -353,12 +405,22 @@ function ChatZap({ streamer, ev }: { streamer: string; ev: TaggedRawEvent }) { } function WriteMessage({ link }: { link: NostrLink }) { + const ref = useRef(null); + const emojiRef = useRef(null); const [chat, setChat] = useState(""); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); const login = useLogin(); - const userEmojis = useEmoji(login!.pubkey); - const channelEmojis = useEmoji(link.author!); + const userEmojiPacks = useEmoji(login!.pubkey); + 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 allEmojiPacks = userEmojiPacks.concat(channelEmojiPacks); + // @ts-expect-error + const topOffset = ref.current?.getBoundingClientRect().top; + // @ts-expect-error + const leftOffset = ref.current?.getBoundingClientRect().left; async function sendChatMessage() { const pub = await EventPublisher.nip7(); @@ -394,6 +456,15 @@ function WriteMessage({ link }: { link: NostrLink }) { } } + function onEmojiSelect(emoji: Emoji) { + if (emoji.native) { + setChat(`${chat}${emoji.native}`); + } else { + setChat(`${chat}:${emoji.id}:`); + } + setShowEmojiPicker(false); + } + async function onKeyDown(e: KeyboardEvent) { if (e.code === "Enter") { e.preventDefault(); @@ -406,15 +477,33 @@ function WriteMessage({ link }: { link: NostrLink }) { setChat(e.target.value); } + function pickEmoji(ev: any) { + ev.stopPropagation(); + setShowEmojiPicker(!showEmojiPicker); + } + return ( <> -
+