fix: chat lag improvements

closes #161 #126
This commit is contained in:
kieran 2024-07-17 17:39:40 +01:00
parent f7f52d1059
commit 4dfe4d9693
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
5 changed files with 154 additions and 121 deletions

View File

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

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

View File

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

View File

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