fix: chat lag improvements

closes #161 #126
This commit is contained in:
2024-07-17 17:39:40 +01:00
parent f7f52d1059
commit 4dfe4d9693
5 changed files with 154 additions and 121 deletions

View File

@ -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",

View 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>
);
}
},
);

View File

@ -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

View File

@ -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;

View File

@ -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"