upd
This commit is contained in:
+66
-6
@@ -3,7 +3,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FullScreen, useFullScreenHandle } from "react-full-screen";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ky from "ky";
|
||||
import { Socket, io } from "socket.io-client";
|
||||
import userAgentParser from "ua-parser-js";
|
||||
@@ -29,6 +29,8 @@ import AlertIcon from "./components/icons/AlertIcon";
|
||||
import useSocketStore from "./stores/useSocketStore";
|
||||
import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react";
|
||||
import ToggleMic from "./components/ToggleMic";
|
||||
import Chat from "./components/Chat";
|
||||
import ChatIcon from "./components/icons/ChatIcon";
|
||||
|
||||
function StreamPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -53,6 +55,10 @@ function StreamPage() {
|
||||
state.users,
|
||||
state.setUsers,
|
||||
]);
|
||||
const [isShowChat, setIsShowChat] = useState<boolean>(false);
|
||||
const [lastActivity, setLastActivity] = useState<Date>(new Date());
|
||||
const lastActivityRef = useRef<Date>();
|
||||
lastActivityRef.current = lastActivity;
|
||||
|
||||
async function getToken() {
|
||||
if (!socket) return;
|
||||
@@ -138,6 +144,34 @@ function StreamPage() {
|
||||
// });
|
||||
// }
|
||||
|
||||
const handleAction = () => {
|
||||
setLastActivity(new Date());
|
||||
};
|
||||
|
||||
const updateLastActivity = async () => {
|
||||
console.log("lastActivity: ", lastActivityRef.current);
|
||||
|
||||
try {
|
||||
const result = await ky
|
||||
.put(`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}`, {
|
||||
json: {
|
||||
lastActivityAt: lastActivityRef.current,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
console.log(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
updateLastActivity();
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
|
||||
@@ -162,11 +196,6 @@ function StreamPage() {
|
||||
setUsers(sockets);
|
||||
});
|
||||
|
||||
socket.on("leave", (socketId, sockets) => {
|
||||
console.log("User disconnected: ", socketId);
|
||||
setUsers(sockets);
|
||||
});
|
||||
|
||||
socket.on("kick", () => {
|
||||
window.close();
|
||||
});
|
||||
@@ -179,8 +208,21 @@ function StreamPage() {
|
||||
);
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", handleAction);
|
||||
document.addEventListener("touchstart", handleAction);
|
||||
|
||||
updateLastActivity();
|
||||
|
||||
return () => {
|
||||
socket.off("connect");
|
||||
socket.off("join");
|
||||
socket.off("update");
|
||||
socket.off("kick");
|
||||
socket.off("leave");
|
||||
socket.close();
|
||||
|
||||
document.removeEventListener("mousemove", handleAction);
|
||||
document.removeEventListener("touchstart", handleAction);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -307,6 +349,16 @@ function StreamPage() {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsShowChat(true)}
|
||||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90 flex justify-center items-center"
|
||||
>
|
||||
<ChatIcon className="w-5 h-5" />
|
||||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||||
<Trans i18nKey={"chat"}>Чат</Trans>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<LiveKitRoom
|
||||
video={false}
|
||||
audio={true}
|
||||
@@ -319,6 +371,14 @@ function StreamPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Transition in={isShowChat} timeout={200} mountOnEnter unmountOnExit>
|
||||
{(state) => (
|
||||
<div className="absolute right-0 bottom-0">
|
||||
<Chat className={state} handleClose={() => setIsShowChat(false)} />
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
in={modal ? true : false}
|
||||
timeout={200}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
.messages::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
transition: 1s color;
|
||||
}
|
||||
|
||||
.messages::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.02); /* color of the tracking area */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.messages::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.05); /* color of the scroll thumb */
|
||||
border-radius: 4px; /* roundness of the scroll thumb */
|
||||
}
|
||||
|
||||
.messages:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1); /* color of the scroll thumb */
|
||||
border-radius: 4px; /* roundness of the scroll thumb */
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||
import useSocketStore from "../stores/useSocketStore";
|
||||
import { UIEvent } from "react";
|
||||
import "./Chat.css";
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import useChatStore from "../stores/useChatStore";
|
||||
|
||||
interface ChatProps {
|
||||
className?: string;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
function Chat({ className, handleClose }: ChatProps) {
|
||||
const socket = useSocketStore((state) => state.socket);
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const [messages, setMessages] = useChatStore((state) => [
|
||||
state.messages,
|
||||
state.setMessages,
|
||||
]);
|
||||
const [isBottom, setIsBottom] = useState<boolean>(true);
|
||||
const [isNewMessage, setIsNewMessage] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function sendMessage(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!socket || !message) return;
|
||||
|
||||
socket.emit("message", socket.id, message);
|
||||
|
||||
setMessage("");
|
||||
inputRef.current?.focus();
|
||||
messagesEndRef.current?.scrollIntoView();
|
||||
}
|
||||
|
||||
const handleScroll = (e: UIEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
e.currentTarget.scrollHeight - Math.round(e.currentTarget.scrollTop) ===
|
||||
e.currentTarget.clientHeight
|
||||
) {
|
||||
setIsBottom(true);
|
||||
setIsNewMessage(false);
|
||||
} else {
|
||||
setIsBottom(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
console.log(messages);
|
||||
|
||||
console.log("CHAT Socket Init: ", socket.id);
|
||||
|
||||
socket.on("message", (socketId, message) => {
|
||||
setMessages([
|
||||
...useChatStore.getState().messages,
|
||||
{ id: socketId, message },
|
||||
]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("message");
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isBottom) {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
} else {
|
||||
setIsNewMessage(true);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"relative p-4 flex flex-col gap-4 w-80 h-screen bg-neutral-900 transition-all",
|
||||
className,
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="border-b pb-3 border-y-neutral-800 flex justify-between">
|
||||
<p className="text-2xl ">Chat</p>
|
||||
<button onClick={handleClose}>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
onScroll={handleScroll}
|
||||
className="messages flex flex-col gap-4 overflow-y-auto overflow-x-hidden flex-1 pr-2"
|
||||
>
|
||||
{messages.map((data, index) => (
|
||||
<p key={index} className="break-words">
|
||||
<b>{data.id}:</b> <span>{data.message}</span>
|
||||
</p>
|
||||
))}
|
||||
|
||||
<div ref={messagesEndRef}></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() =>
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
className={[
|
||||
"absolute mr-8 right-0 bottom-[136px] bg-neutral-950 p-2 rounded-full shadow transition-opacity",
|
||||
isBottom
|
||||
? "opacity-0 pointer-events-none cursor-auto"
|
||||
: "opacity-100 pointer-events-auto cursor-pointer",
|
||||
].join(" ")}
|
||||
>
|
||||
<ChevronDownIcon className="w-5 h-5" />
|
||||
|
||||
<div
|
||||
className={[
|
||||
"absolute top-0 right-0 w-3 h-3 flex justify-center items-center rounded-full bg-red-500 shadow transition-opacity",
|
||||
isNewMessage ? "opacity-100" : "opacity-0",
|
||||
].join(" ")}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={sendMessage} className="flex flex-col gap-4">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Message"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="px-3 py-2 outline-none rounded"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="p-2 bg-blue-700 hover:bg-blue-800 transition-colors outline-none rounded"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Chat;
|
||||
@@ -0,0 +1,19 @@
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ChatIcon({ className }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path d="M4.913 2.658c2.075-.27 4.19-.408 6.337-.408 2.147 0 4.262.139 6.337.408 1.922.25 3.291 1.861 3.405 3.727a4.403 4.403 0 00-1.032-.211 50.89 50.89 0 00-8.42 0c-2.358.196-4.04 2.19-4.04 4.434v4.286a4.47 4.47 0 002.433 3.984L7.28 21.53A.75.75 0 016 21v-4.03a48.527 48.527 0 01-1.087-.128C2.905 16.58 1.5 14.833 1.5 12.862V6.638c0-1.97 1.405-3.718 3.413-3.979z" />
|
||||
<path d="M15.75 7.5c-1.376 0-2.739.057-4.086.169C10.124 7.797 9 9.103 9 10.609v4.285c0 1.507 1.128 2.814 2.67 2.94 1.243.102 2.5.157 3.768.165l2.782 2.781a.75.75 0 001.28-.53v-2.39l.33-.026c1.542-.125 2.67-1.433 2.67-2.94v-4.286c0-1.505-1.125-2.811-2.664-2.94A49.392 49.392 0 0015.75 7.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatIcon;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
interface ChatState {
|
||||
messages: any[];
|
||||
setMessages: (messages: any[]) => void;
|
||||
}
|
||||
|
||||
const useChatStore = create<ChatState>()(
|
||||
devtools((set) => ({
|
||||
messages: [],
|
||||
setMessages: (messages) => set({ messages }),
|
||||
}))
|
||||
);
|
||||
|
||||
export default useChatStore;
|
||||
Reference in New Issue
Block a user