parent
f7f52d1059
commit
4dfe4d9693
@ -8,10 +8,10 @@
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@scure/base": "^1.1.6",
|
||||
"@snort/shared": "^1.0.16",
|
||||
"@snort/system": "^1.4.0",
|
||||
"@snort/system-react": "^1.4.0",
|
||||
"@snort/system": "^1.4.2",
|
||||
"@snort/system-react": "^1.4.2",
|
||||
"@snort/system-wasm": "^1.0.4",
|
||||
"@snort/wallet": "^0.1.6",
|
||||
"@snort/wallet": "^0.1.7",
|
||||
"@snort/worker-relay": "^1.1.0",
|
||||
"@sqlite.org/sqlite-wasm": "^3.45.1-build1",
|
||||
"@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 { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
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 dayjs from "dayjs";
|
||||
|
||||
@ -17,10 +17,10 @@ import { CollapsibleEvent } from "../collapsible";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import { formatSats } from "@/number";
|
||||
import type { Badge, Emoji, EmojiPack } from "@/types";
|
||||
import { IconButton } from "../buttons";
|
||||
import Pill from "../pill";
|
||||
import classNames from "classnames";
|
||||
import Modal from "../modal";
|
||||
import { ChatMenu } from "./chat-menu";
|
||||
|
||||
function emojifyReaction(reaction: string) {
|
||||
if (reaction === "+") {
|
||||
@ -46,17 +46,24 @@ export function ChatMessage({
|
||||
const system = useContext(SnortContext);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const emojiRef = useRef(null);
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
const isHovering = useHover(ref);
|
||||
const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]);
|
||||
const { mute } = useMute(ev.pubkey);
|
||||
const [showZapDialog, setShowZapDialog] = useState(false);
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const [zapping, setZapping] = useState(false);
|
||||
const login = useLogin();
|
||||
const profile = useUserProfile(ev.pubkey);
|
||||
const shouldShowMuteButton = ev.pubkey !== streamer && ev.pubkey !== login?.pubkey;
|
||||
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 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));
|
||||
|
||||
useOnClickOutside(ref, () => {
|
||||
setShowZapDialog(false);
|
||||
setZapping(false);
|
||||
});
|
||||
|
||||
useOnClickOutside(emojiRef, () => {
|
||||
@ -85,7 +92,7 @@ export function ChatMessage({
|
||||
|
||||
async function onEmojiSelect(emoji: Emoji) {
|
||||
setShowEmojiPicker(false);
|
||||
setShowZapDialog(false);
|
||||
setZapping(false);
|
||||
let reply = null;
|
||||
try {
|
||||
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) {
|
||||
e.stopPropagation();
|
||||
setShowEmojiPicker(!showEmojiPicker);
|
||||
@ -126,6 +130,9 @@ export function ChatMessage({
|
||||
mute();
|
||||
}
|
||||
|
||||
const topOffset = ref?.current?.getBoundingClientRect().top;
|
||||
const leftOffset = ref?.current?.getBoundingClientRect().left;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="leading-6 overflow-wrap" ref={ref}>
|
||||
@ -165,39 +172,14 @@ export function ChatMessage({
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{ref.current && isHovering && (
|
||||
<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: showZapDialog || isHovering ? 1 : 0,
|
||||
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>
|
||||
)}
|
||||
<ChatMenu
|
||||
ref={ref}
|
||||
zapTarget={zapTarget}
|
||||
onPickEmoji={pickEmoji}
|
||||
onMuteUser={muteUser}
|
||||
onZapping={() => setZapping(true)}
|
||||
showMuteButton={shouldShowMuteButton}
|
||||
/>
|
||||
{zapping && zapTarget && (
|
||||
<Modal id="send-zaps" onClose={() => setZapping(false)}>
|
||||
<SendZaps
|
||||
|
@ -1,6 +1,6 @@
|
||||
import data, { Emoji } from "@emoji-mart/data";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import { RefObject } from "react";
|
||||
import { RefObject, forwardRef } from "react";
|
||||
import { EmojiPack } from "@/types";
|
||||
|
||||
interface EmojiPickerProps {
|
||||
@ -13,57 +13,53 @@ interface EmojiPickerProps {
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export default 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 (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: topOffset - height - 10,
|
||||
left: leftOffset,
|
||||
zIndex: 1,
|
||||
}}
|
||||
ref={ref}>
|
||||
<style>
|
||||
{`
|
||||
const EmojiPicker = forwardRef<HTMLDivElement | null, EmojiPickerProps>(
|
||||
({ topOffset, leftOffset, onEmojiSelect, onClickOutside, emojiPacks = [], height = 300 }, ref) => {
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: topOffset - height - 10,
|
||||
left: leftOffset,
|
||||
zIndex: 1,
|
||||
}}
|
||||
ref={ref}>
|
||||
<style>
|
||||
{`
|
||||
em-emoji-picker { max-height: ${height}px; }
|
||||
`}
|
||||
</style>
|
||||
<Picker
|
||||
autoFocus
|
||||
data={data}
|
||||
custom={customEmojiList}
|
||||
perLine={7}
|
||||
previewPosition="none"
|
||||
skinTonePosition="search"
|
||||
theme="dark"
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
onClickOutside={onClickOutside}
|
||||
maxFrequentRows={0}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
</style>
|
||||
<Picker
|
||||
autoFocus
|
||||
data={data}
|
||||
custom={customEmojiList}
|
||||
perLine={7}
|
||||
previewPosition="none"
|
||||
skinTonePosition="search"
|
||||
theme="dark"
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
onClickOutside={onClickOutside}
|
||||
maxFrequentRows={0}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default EmojiPicker;
|
||||
|
42
yarn.lock
42
yarn.lock
@ -2644,14 +2644,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/system-react@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "@snort/system-react@npm:1.4.0"
|
||||
"@snort/system-react@npm:^1.4.2":
|
||||
version: 1.4.2
|
||||
resolution: "@snort/system-react@npm:1.4.2"
|
||||
dependencies:
|
||||
"@snort/shared": "npm:^1.0.16"
|
||||
"@snort/system": "npm:^1.4.0"
|
||||
"@snort/system": "npm:^1.4.2"
|
||||
react: "npm:^18.2.0"
|
||||
checksum: 10c0/6ab54cf979e3135d896f74737be8314cf9924ade0657b0d43fb2b382e68a8025e7c20ce9e7608214accaeb854885b34f26d145b40ac8e37f0fbf453260d0cb63
|
||||
checksum: 10c0/e39e88cae30ec1d2ced09da8e3434269ea990e4de74a70d5f8c468a8cd4ed73462f38d702f61c148a2ca44420f05d66368e2a5b9dc5f7f88abaf309cb2da0c71
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -2662,9 +2662,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/system@npm:^1.3.8":
|
||||
version: 1.3.8
|
||||
resolution: "@snort/system@npm:1.3.8"
|
||||
"@snort/system@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "@snort/system@npm:1.4.1"
|
||||
dependencies:
|
||||
"@noble/curves": "npm:^1.4.0"
|
||||
"@noble/hashes": "npm:^1.4.0"
|
||||
@ -2679,13 +2679,13 @@ __metadata:
|
||||
lru-cache: "npm:^10.2.0"
|
||||
uuid: "npm:^9.0.0"
|
||||
ws: "npm:^8.14.0"
|
||||
checksum: 10c0/fb124dc6687176614eca0fc4aa496d2891690fff701ff7acf6b5c01dbda4aa419c6da8d68309717f00f8f60981e636d0da86bab07710e2d3c27ae7e0fa841136
|
||||
checksum: 10c0/458aab52c3e2a42c4b8bcf003846e647c60e4308b87139a9cea11ee8c777a1320dde60f21722e7720737a229a2a19a11b3e3c5ab04d9e764e44fa41f310c81fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/system@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "@snort/system@npm:1.4.0"
|
||||
"@snort/system@npm:^1.4.2":
|
||||
version: 1.4.2
|
||||
resolution: "@snort/system@npm:1.4.2"
|
||||
dependencies:
|
||||
"@noble/curves": "npm:^1.4.0"
|
||||
"@noble/hashes": "npm:^1.4.0"
|
||||
@ -2700,22 +2700,22 @@ __metadata:
|
||||
lru-cache: "npm:^10.2.0"
|
||||
uuid: "npm:^9.0.0"
|
||||
ws: "npm:^8.14.0"
|
||||
checksum: 10c0/655b3da80815a4494ba5ea3d6f390d8344b2e9e14a3b59cdef39dee6666d01788811dddf0641a76538964b0168201d2e142ee744c296eee4af29bb7f5c36310d
|
||||
checksum: 10c0/7da08a01fd0f1029117a3a74abba4fbbb8ddf9310cb7eb1b0fec832d481bb149134b2c3cd167df9dc375fb3dfa076721e2362c76f889b03dff11adf8fbd7bd20
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/wallet@npm:^0.1.6":
|
||||
version: 0.1.6
|
||||
resolution: "@snort/wallet@npm:0.1.6"
|
||||
"@snort/wallet@npm:^0.1.7":
|
||||
version: 0.1.7
|
||||
resolution: "@snort/wallet@npm:0.1.7"
|
||||
dependencies:
|
||||
"@cashu/cashu-ts": "npm:^1.0.0-rc.3"
|
||||
"@lightninglabs/lnc-web": "npm:^0.3.1-alpha"
|
||||
"@scure/base": "npm:^1.1.6"
|
||||
"@snort/shared": "npm:^1.0.16"
|
||||
"@snort/system": "npm:^1.3.8"
|
||||
"@snort/system": "npm:^1.4.1"
|
||||
debug: "npm:^4.3.4"
|
||||
eventemitter3: "npm:^5.0.1"
|
||||
checksum: 10c0/49ed6ec1abde6960db3ffb715d43698dcb826465c3116457a1a5929e7b186ffdc8ee71ef0ed344be54334d392167e591ad936685d1238b5453198f6f7f3abdd4
|
||||
checksum: 10c0/5063b1f17337d498d0e466b8e3ec083d2e313df383d882b7900e04187126f07710566c74fdddcf90455f32f2d832cfbfa0a1a3f9c33cd09b7fb9913826b15cda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -7619,10 +7619,10 @@ __metadata:
|
||||
"@noble/hashes": "npm:^1.4.0"
|
||||
"@scure/base": "npm:^1.1.6"
|
||||
"@snort/shared": "npm:^1.0.16"
|
||||
"@snort/system": "npm:^1.4.0"
|
||||
"@snort/system-react": "npm:^1.4.0"
|
||||
"@snort/system": "npm:^1.4.2"
|
||||
"@snort/system-react": "npm:^1.4.2"
|
||||
"@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"
|
||||
"@sqlite.org/sqlite-wasm": "npm:^3.45.1-build1"
|
||||
"@szhsin/react-menu": "npm:^4.1.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user