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"