@ -8,10 +8,10 @@
|
|||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@scure/base": "^1.1.6",
|
"@scure/base": "^1.1.6",
|
||||||
"@snort/shared": "^1.0.16",
|
"@snort/shared": "^1.0.16",
|
||||||
"@snort/system": "^1.4.0",
|
"@snort/system": "^1.4.2",
|
||||||
"@snort/system-react": "^1.4.0",
|
"@snort/system-react": "^1.4.2",
|
||||||
"@snort/system-wasm": "^1.0.4",
|
"@snort/system-wasm": "^1.0.4",
|
||||||
"@snort/wallet": "^0.1.6",
|
"@snort/wallet": "^0.1.7",
|
||||||
"@snort/worker-relay": "^1.1.0",
|
"@snort/worker-relay": "^1.1.0",
|
||||||
"@sqlite.org/sqlite-wasm": "^3.45.1-build1",
|
"@sqlite.org/sqlite-wasm": "^3.45.1-build1",
|
||||||
"@szhsin/react-menu": "^4.1.0",
|
"@szhsin/react-menu": "^4.1.0",
|
||||||
|
55
src/element/chat/chat-menu.tsx
Normal file
55
src/element/chat/chat-menu.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useHover } from "usehooks-ts";
|
||||||
|
import { IconButton } from "../buttons";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
|
interface ChatMenuProps {
|
||||||
|
zapTarget?: string;
|
||||||
|
onPickEmoji: (e: React.MouseEvent) => void;
|
||||||
|
onMuteUser: (e: React.MouseEvent) => void;
|
||||||
|
onZapping: (e: React.MouseEvent) => void;
|
||||||
|
showMuteButton?: boolean;
|
||||||
|
}
|
||||||
|
export const ChatMenu = forwardRef<HTMLDivElement | null, ChatMenuProps>(
|
||||||
|
({ zapTarget, onPickEmoji, onMuteUser, onZapping, showMuteButton }, ref) => {
|
||||||
|
if (!ref || !("current" in ref)) return;
|
||||||
|
const topOffset = ref?.current?.getBoundingClientRect().top;
|
||||||
|
const leftOffset = ref?.current?.getBoundingClientRect().left;
|
||||||
|
|
||||||
|
const isHovering = useHover(ref);
|
||||||
|
if (ref?.current && isHovering) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed rounded-lg p-2 bg-layer-1 border border-layer-2 flex gap-1 z-10"
|
||||||
|
style={{
|
||||||
|
top: topOffset ? topOffset + 24 : 0,
|
||||||
|
left: leftOffset ? leftOffset : 0,
|
||||||
|
opacity: isHovering ? 1 : 0,
|
||||||
|
pointerEvents: isHovering ? "auto" : "none",
|
||||||
|
}}>
|
||||||
|
{zapTarget && (
|
||||||
|
<IconButton
|
||||||
|
iconName="zap"
|
||||||
|
iconSize={14}
|
||||||
|
className="p-2 rounded-full bg-layer-2 aspect-square"
|
||||||
|
onClick={onZapping}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
onClick={onPickEmoji}
|
||||||
|
iconName="face"
|
||||||
|
iconSize={14}
|
||||||
|
className="p-2 rounded-full bg-layer-2 aspect-square"
|
||||||
|
/>
|
||||||
|
{showMuteButton && (
|
||||||
|
<IconButton
|
||||||
|
onClick={onMuteUser}
|
||||||
|
iconName="user-x"
|
||||||
|
iconSize={14}
|
||||||
|
className="p-2 rounded-full bg-layer-2 aspect-square"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
@ -1,7 +1,7 @@
|
|||||||
import { SnortContext, useEventReactions, useReactions, useUserProfile } from "@snort/system-react";
|
import { SnortContext, useEventReactions, useReactions, useUserProfile } from "@snort/system-react";
|
||||||
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import React, { Suspense, lazy, useContext, useMemo, useRef, useState } from "react";
|
import React, { Suspense, lazy, useContext, useMemo, useRef, useState } from "react";
|
||||||
import { useHover, useOnClickOutside } from "usehooks-ts";
|
import { useOnClickOutside } from "usehooks-ts";
|
||||||
import { dedupe } from "@snort/shared";
|
import { dedupe } from "@snort/shared";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@ -17,10 +17,10 @@ import { CollapsibleEvent } from "../collapsible";
|
|||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { formatSats } from "@/number";
|
import { formatSats } from "@/number";
|
||||||
import type { Badge, Emoji, EmojiPack } from "@/types";
|
import type { Badge, Emoji, EmojiPack } from "@/types";
|
||||||
import { IconButton } from "../buttons";
|
|
||||||
import Pill from "../pill";
|
import Pill from "../pill";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Modal from "../modal";
|
import Modal from "../modal";
|
||||||
|
import { ChatMenu } from "./chat-menu";
|
||||||
|
|
||||||
function emojifyReaction(reaction: string) {
|
function emojifyReaction(reaction: string) {
|
||||||
if (reaction === "+") {
|
if (reaction === "+") {
|
||||||
@ -46,17 +46,24 @@ export function ChatMessage({
|
|||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const emojiRef = useRef(null);
|
const emojiRef = useRef(null);
|
||||||
const link = NostrLink.fromEvent(ev);
|
const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]);
|
||||||
const isHovering = useHover(ref);
|
|
||||||
const { mute } = useMute(ev.pubkey);
|
const { mute } = useMute(ev.pubkey);
|
||||||
const [showZapDialog, setShowZapDialog] = useState(false);
|
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const [zapping, setZapping] = useState(false);
|
const [zapping, setZapping] = useState(false);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const profile = useUserProfile(ev.pubkey);
|
const profile = useUserProfile(ev.pubkey);
|
||||||
const shouldShowMuteButton = ev.pubkey !== streamer && ev.pubkey !== login?.pubkey;
|
const shouldShowMuteButton = ev.pubkey !== streamer && ev.pubkey !== login?.pubkey;
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
const related = useReactions("reactions", [link], undefined, false);
|
const related = useReactions(
|
||||||
|
"reactions",
|
||||||
|
link,
|
||||||
|
rb => {
|
||||||
|
rb.withOptions({
|
||||||
|
replaceable: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
const { zaps, reactions } = useEventReactions(link, related);
|
const { zaps, reactions } = useEventReactions(link, related);
|
||||||
const emojiNames = emojiPacks.map(p => p.emojis).flat();
|
const emojiNames = emojiPacks.map(p => p.emojis).flat();
|
||||||
|
|
||||||
@ -72,7 +79,7 @@ export function ChatMessage({
|
|||||||
const awardedBadges = badges.filter(b => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey));
|
const awardedBadges = badges.filter(b => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey));
|
||||||
|
|
||||||
useOnClickOutside(ref, () => {
|
useOnClickOutside(ref, () => {
|
||||||
setShowZapDialog(false);
|
setZapping(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
useOnClickOutside(emojiRef, () => {
|
useOnClickOutside(emojiRef, () => {
|
||||||
@ -85,7 +92,7 @@ export function ChatMessage({
|
|||||||
|
|
||||||
async function onEmojiSelect(emoji: Emoji) {
|
async function onEmojiSelect(emoji: Emoji) {
|
||||||
setShowEmojiPicker(false);
|
setShowEmojiPicker(false);
|
||||||
setShowZapDialog(false);
|
setZapping(false);
|
||||||
let reply = null;
|
let reply = null;
|
||||||
try {
|
try {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
@ -113,9 +120,6 @@ export function ChatMessage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const topOffset = ref.current?.getBoundingClientRect().top;
|
|
||||||
const leftOffset = ref.current?.getBoundingClientRect().left;
|
|
||||||
|
|
||||||
function pickEmoji(e: React.MouseEvent) {
|
function pickEmoji(e: React.MouseEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowEmojiPicker(!showEmojiPicker);
|
setShowEmojiPicker(!showEmojiPicker);
|
||||||
@ -126,6 +130,9 @@ export function ChatMessage({
|
|||||||
mute();
|
mute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const topOffset = ref?.current?.getBoundingClientRect().top;
|
||||||
|
const leftOffset = ref?.current?.getBoundingClientRect().left;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="leading-6 overflow-wrap" ref={ref}>
|
<div className="leading-6 overflow-wrap" ref={ref}>
|
||||||
@ -165,39 +172,14 @@ export function ChatMessage({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ref.current && isHovering && (
|
<ChatMenu
|
||||||
<div
|
ref={ref}
|
||||||
className="fixed rounded-lg p-2 bg-layer-1 border border-layer-2 flex gap-1 z-10"
|
zapTarget={zapTarget}
|
||||||
style={{
|
onPickEmoji={pickEmoji}
|
||||||
top: topOffset ? topOffset + 24 : 0,
|
onMuteUser={muteUser}
|
||||||
left: leftOffset ? leftOffset : 0,
|
onZapping={() => setZapping(true)}
|
||||||
opacity: showZapDialog || isHovering ? 1 : 0,
|
showMuteButton={shouldShowMuteButton}
|
||||||
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
|
|
||||||
}}>
|
|
||||||
{zapTarget && (
|
|
||||||
<IconButton
|
|
||||||
iconName="zap"
|
|
||||||
iconSize={14}
|
|
||||||
className="p-2 rounded-full bg-layer-2 aspect-square"
|
|
||||||
onClick={() => setZapping(true)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
onClick={pickEmoji}
|
|
||||||
iconName="face"
|
|
||||||
iconSize={14}
|
|
||||||
className="p-2 rounded-full bg-layer-2 aspect-square"
|
|
||||||
/>
|
|
||||||
{shouldShowMuteButton && (
|
|
||||||
<IconButton
|
|
||||||
onClick={muteUser}
|
|
||||||
iconName="user-x"
|
|
||||||
iconSize={14}
|
|
||||||
className="p-2 rounded-full bg-layer-2 aspect-square"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{zapping && zapTarget && (
|
{zapping && zapTarget && (
|
||||||
<Modal id="send-zaps" onClose={() => setZapping(false)}>
|
<Modal id="send-zaps" onClose={() => setZapping(false)}>
|
||||||
<SendZaps
|
<SendZaps
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import data, { Emoji } from "@emoji-mart/data";
|
import data, { Emoji } from "@emoji-mart/data";
|
||||||
import Picker from "@emoji-mart/react";
|
import Picker from "@emoji-mart/react";
|
||||||
import { RefObject } from "react";
|
import { RefObject, forwardRef } from "react";
|
||||||
import { EmojiPack } from "@/types";
|
import { EmojiPack } from "@/types";
|
||||||
|
|
||||||
interface EmojiPickerProps {
|
interface EmojiPickerProps {
|
||||||
@ -13,15 +13,8 @@ interface EmojiPickerProps {
|
|||||||
ref: RefObject<HTMLDivElement>;
|
ref: RefObject<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmojiPicker({
|
const EmojiPicker = forwardRef<HTMLDivElement | null, EmojiPickerProps>(
|
||||||
topOffset,
|
({ topOffset, leftOffset, onEmojiSelect, onClickOutside, emojiPacks = [], height = 300 }, ref) => {
|
||||||
leftOffset,
|
|
||||||
onEmojiSelect,
|
|
||||||
onClickOutside,
|
|
||||||
emojiPacks = [],
|
|
||||||
height = 300,
|
|
||||||
ref,
|
|
||||||
}: EmojiPickerProps) {
|
|
||||||
const customEmojiList = emojiPacks.map(pack => {
|
const customEmojiList = emojiPacks.map(pack => {
|
||||||
return {
|
return {
|
||||||
id: pack.address,
|
id: pack.address,
|
||||||
@ -66,4 +59,7 @@ export default function EmojiPicker({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EmojiPicker;
|
||||||
|
42
yarn.lock
42
yarn.lock
@ -2644,14 +2644,14 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/system-react@npm:^1.4.0":
|
"@snort/system-react@npm:^1.4.2":
|
||||||
version: 1.4.0
|
version: 1.4.2
|
||||||
resolution: "@snort/system-react@npm:1.4.0"
|
resolution: "@snort/system-react@npm:1.4.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@snort/shared": "npm:^1.0.16"
|
"@snort/shared": "npm:^1.0.16"
|
||||||
"@snort/system": "npm:^1.4.0"
|
"@snort/system": "npm:^1.4.2"
|
||||||
react: "npm:^18.2.0"
|
react: "npm:^18.2.0"
|
||||||
checksum: 10c0/6ab54cf979e3135d896f74737be8314cf9924ade0657b0d43fb2b382e68a8025e7c20ce9e7608214accaeb854885b34f26d145b40ac8e37f0fbf453260d0cb63
|
checksum: 10c0/e39e88cae30ec1d2ced09da8e3434269ea990e4de74a70d5f8c468a8cd4ed73462f38d702f61c148a2ca44420f05d66368e2a5b9dc5f7f88abaf309cb2da0c71
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -2662,9 +2662,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/system@npm:^1.3.8":
|
"@snort/system@npm:^1.4.1":
|
||||||
version: 1.3.8
|
version: 1.4.1
|
||||||
resolution: "@snort/system@npm:1.3.8"
|
resolution: "@snort/system@npm:1.4.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@noble/curves": "npm:^1.4.0"
|
"@noble/curves": "npm:^1.4.0"
|
||||||
"@noble/hashes": "npm:^1.4.0"
|
"@noble/hashes": "npm:^1.4.0"
|
||||||
@ -2679,13 +2679,13 @@ __metadata:
|
|||||||
lru-cache: "npm:^10.2.0"
|
lru-cache: "npm:^10.2.0"
|
||||||
uuid: "npm:^9.0.0"
|
uuid: "npm:^9.0.0"
|
||||||
ws: "npm:^8.14.0"
|
ws: "npm:^8.14.0"
|
||||||
checksum: 10c0/fb124dc6687176614eca0fc4aa496d2891690fff701ff7acf6b5c01dbda4aa419c6da8d68309717f00f8f60981e636d0da86bab07710e2d3c27ae7e0fa841136
|
checksum: 10c0/458aab52c3e2a42c4b8bcf003846e647c60e4308b87139a9cea11ee8c777a1320dde60f21722e7720737a229a2a19a11b3e3c5ab04d9e764e44fa41f310c81fd
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/system@npm:^1.4.0":
|
"@snort/system@npm:^1.4.2":
|
||||||
version: 1.4.0
|
version: 1.4.2
|
||||||
resolution: "@snort/system@npm:1.4.0"
|
resolution: "@snort/system@npm:1.4.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@noble/curves": "npm:^1.4.0"
|
"@noble/curves": "npm:^1.4.0"
|
||||||
"@noble/hashes": "npm:^1.4.0"
|
"@noble/hashes": "npm:^1.4.0"
|
||||||
@ -2700,22 +2700,22 @@ __metadata:
|
|||||||
lru-cache: "npm:^10.2.0"
|
lru-cache: "npm:^10.2.0"
|
||||||
uuid: "npm:^9.0.0"
|
uuid: "npm:^9.0.0"
|
||||||
ws: "npm:^8.14.0"
|
ws: "npm:^8.14.0"
|
||||||
checksum: 10c0/655b3da80815a4494ba5ea3d6f390d8344b2e9e14a3b59cdef39dee6666d01788811dddf0641a76538964b0168201d2e142ee744c296eee4af29bb7f5c36310d
|
checksum: 10c0/7da08a01fd0f1029117a3a74abba4fbbb8ddf9310cb7eb1b0fec832d481bb149134b2c3cd167df9dc375fb3dfa076721e2362c76f889b03dff11adf8fbd7bd20
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/wallet@npm:^0.1.6":
|
"@snort/wallet@npm:^0.1.7":
|
||||||
version: 0.1.6
|
version: 0.1.7
|
||||||
resolution: "@snort/wallet@npm:0.1.6"
|
resolution: "@snort/wallet@npm:0.1.7"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@cashu/cashu-ts": "npm:^1.0.0-rc.3"
|
"@cashu/cashu-ts": "npm:^1.0.0-rc.3"
|
||||||
"@lightninglabs/lnc-web": "npm:^0.3.1-alpha"
|
"@lightninglabs/lnc-web": "npm:^0.3.1-alpha"
|
||||||
"@scure/base": "npm:^1.1.6"
|
"@scure/base": "npm:^1.1.6"
|
||||||
"@snort/shared": "npm:^1.0.16"
|
"@snort/shared": "npm:^1.0.16"
|
||||||
"@snort/system": "npm:^1.3.8"
|
"@snort/system": "npm:^1.4.1"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
eventemitter3: "npm:^5.0.1"
|
eventemitter3: "npm:^5.0.1"
|
||||||
checksum: 10c0/49ed6ec1abde6960db3ffb715d43698dcb826465c3116457a1a5929e7b186ffdc8ee71ef0ed344be54334d392167e591ad936685d1238b5453198f6f7f3abdd4
|
checksum: 10c0/5063b1f17337d498d0e466b8e3ec083d2e313df383d882b7900e04187126f07710566c74fdddcf90455f32f2d832cfbfa0a1a3f9c33cd09b7fb9913826b15cda
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -7619,10 +7619,10 @@ __metadata:
|
|||||||
"@noble/hashes": "npm:^1.4.0"
|
"@noble/hashes": "npm:^1.4.0"
|
||||||
"@scure/base": "npm:^1.1.6"
|
"@scure/base": "npm:^1.1.6"
|
||||||
"@snort/shared": "npm:^1.0.16"
|
"@snort/shared": "npm:^1.0.16"
|
||||||
"@snort/system": "npm:^1.4.0"
|
"@snort/system": "npm:^1.4.2"
|
||||||
"@snort/system-react": "npm:^1.4.0"
|
"@snort/system-react": "npm:^1.4.2"
|
||||||
"@snort/system-wasm": "npm:^1.0.4"
|
"@snort/system-wasm": "npm:^1.0.4"
|
||||||
"@snort/wallet": "npm:^0.1.6"
|
"@snort/wallet": "npm:^0.1.7"
|
||||||
"@snort/worker-relay": "npm:^1.1.0"
|
"@snort/worker-relay": "npm:^1.1.0"
|
||||||
"@sqlite.org/sqlite-wasm": "npm:^3.45.1-build1"
|
"@sqlite.org/sqlite-wasm": "npm:^3.45.1-build1"
|
||||||
"@szhsin/react-menu": "npm:^4.1.0"
|
"@szhsin/react-menu": "npm:^4.1.0"
|
||||||
|
Reference in New Issue
Block a user