462 lines
15 KiB
TypeScript
462 lines
15 KiB
TypeScript
import { FiActivity, FiSun } from "react-icons/fi";
|
|
import {
|
|
RtcAnswerRequest,
|
|
RtcCallRequest,
|
|
RtcNewUserResponse,
|
|
RtcReceiveAnswerResponse,
|
|
ServerResponseChannels,
|
|
SomeoneUntunedFromLineResponse,
|
|
} from "@nirvana/core/sockets/channels";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import { $selectedLineId } from "../../../controller/recoil";
|
|
import { Avatar } from "antd";
|
|
import LineIcon from "../lineIcon";
|
|
import { LineMemberState } from "@nirvana/core/models/line.model";
|
|
import MasterLineData from "@nirvana/core/models/masterLineData.model";
|
|
import Peer from "simple-peer";
|
|
import { ServerRequestChannels } from "../../../../../core/sockets/channels";
|
|
import moment from "moment";
|
|
import toast from "react-hot-toast";
|
|
import { useGetUserDetails } from "../../../controller/index";
|
|
import { useLineDataProvider } from "../../../controller/lineDataProvider";
|
|
import { useRecoilState } from "recoil";
|
|
|
|
// todo: send a much more comprehensive master line object? or just add properties to the
|
|
// masterLineData object so that we don't have different models to maintain between client and server
|
|
|
|
export default function LineRow({
|
|
masterLineData,
|
|
handleSelectLine,
|
|
}: {
|
|
masterLineData: MasterLineData;
|
|
handleSelectLine: (lineId: string) => void;
|
|
}) {
|
|
const [selectedLineId, setSelectedLineId] = useRecoilState($selectedLineId);
|
|
const { data: userData } = useGetUserDetails();
|
|
|
|
useEffect(() => {
|
|
console.warn("mounting linerow");
|
|
|
|
return () => console.warn("UNMOUNTING line row");
|
|
}, []);
|
|
|
|
// take the source of truth list of memeberIds tuned in, and see if I'm in it
|
|
const isUserTunedIn = useMemo(
|
|
() =>
|
|
masterLineData.tunedInMemberIds?.includes(userData?.user?._id.toString()),
|
|
[masterLineData.tunedInMemberIds, userData]
|
|
);
|
|
|
|
/**
|
|
* TODO: slowly add to this and fix based on added features
|
|
* */
|
|
const renderActivityIcon = useMemo(() => {
|
|
if (isUserTunedIn)
|
|
return (
|
|
<span className="h-2 w-2">
|
|
<FiActivity className="text-black" />
|
|
</span>
|
|
);
|
|
|
|
// if there is someone or me broadcasting here
|
|
if (masterLineData.currentBroadcastersUserIds?.length > 0)
|
|
return <FiSun className="text-xs text-teal-500" />;
|
|
|
|
// if there is new activity blocks for me
|
|
if (masterLineData.currentUserMember.lastVisitDate)
|
|
return (
|
|
<span className="h-2 w-2 rounded-full bg-slate-800 animate-pulse"></span>
|
|
);
|
|
|
|
return (
|
|
<span className="h-2 w-2 rounded-full bg-white animate-pulse"></span>
|
|
);
|
|
}, [masterLineData, isUserTunedIn]);
|
|
|
|
const renderRightActivity = useMemo(() => {
|
|
// TODO: get the profile pictures of the broadcasters
|
|
if (masterLineData.currentBroadcastersUserIds?.length > 0)
|
|
return (
|
|
<Avatar.Group
|
|
key={`lineRowRightActivityGroup-${masterLineData.lineDetails._id.toString()}`}
|
|
maxCount={2}
|
|
maxPopoverTrigger="click"
|
|
size="small"
|
|
maxStyle={{
|
|
color: "#f56a00",
|
|
backgroundColor: "#fde3cf",
|
|
cursor: "pointer",
|
|
borderRadius: "0",
|
|
}}
|
|
className="shadow-lg"
|
|
>
|
|
{masterLineData.otherUserObjects?.map((otherUser, index) => (
|
|
<Avatar
|
|
key={`lineListActivitySection-${otherUser._id.toString()}-${index}`}
|
|
src={otherUser.picture ?? ""}
|
|
shape="square"
|
|
size={"small"}
|
|
/>
|
|
))}
|
|
</Avatar.Group>
|
|
);
|
|
|
|
// if there is new activity/black dot, then show relative time as little bolder? or too much?
|
|
|
|
// TODO: compare last visit date to latest audio block
|
|
if (masterLineData.currentUserMember.lastVisitDate)
|
|
return (
|
|
<span className={`text-gray-400 ml-auto text-xs font-semibold`}>
|
|
{moment(masterLineData.currentUserMember.lastVisitDate).fromNow(true)}
|
|
</span>
|
|
);
|
|
|
|
return (
|
|
<span className={`text-gray-300 ml-auto text-xs `}>
|
|
{moment(masterLineData.currentUserMember.lastVisitDate).fromNow(true)}
|
|
</span>
|
|
);
|
|
}, [masterLineData]);
|
|
|
|
const profilePictures = useMemo(() => {
|
|
const pictureSources: string[] = [];
|
|
|
|
// ?don't add in my image as that's useless contextually?
|
|
// if (userData?.user?.picture) pictureSources.push(userData.user.picture);
|
|
|
|
masterLineData.otherUserObjects?.forEach((otherUser) => {
|
|
if (otherUser.picture) pictureSources.push(otherUser.picture);
|
|
});
|
|
|
|
return pictureSources;
|
|
}, [masterLineData, userData]);
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
onClick={() =>
|
|
handleSelectLine(masterLineData.lineDetails._id.toString())
|
|
}
|
|
className={`flex flex-row items-center justify-start gap-2 p-2 px-4 h-14 hover:bg-gray-200 cursor-pointer transition-all
|
|
last:border-b-0 border-b border-b-gray-200 relative z-50 ${
|
|
selectedLineId === masterLineData.lineDetails._id.toString() &&
|
|
"bg-gray-200 scale-110 shadow-2xl translate-x-3"
|
|
}`}
|
|
>
|
|
{/* status dot */}
|
|
<div className="flex-shrink-0 h-4 w-4">{renderActivityIcon}</div>
|
|
|
|
{profilePictures && (
|
|
<LineIcon grayscale={!isUserTunedIn} sourceImages={profilePictures} />
|
|
)}
|
|
|
|
<h2
|
|
className={`text-inherit text-md max-w-[220px] truncate text-slate-800 ${
|
|
masterLineData.currentUserMember.lastVisitDate
|
|
? "font-semibold"
|
|
: ""
|
|
}`}
|
|
>
|
|
{masterLineData.lineDetails.name ||
|
|
masterLineData.otherUserObjects[0].givenName}
|
|
</h2>
|
|
|
|
<div className="ml-auto flex-shrink-0">{renderRightActivity}</div>
|
|
</div>
|
|
|
|
{/* mounts and unmounts based on if in the room or now */}
|
|
{/* {isUserTunedIn && (
|
|
<StreamRoom
|
|
lineId={masterLineData.lineDetails._id.toString()}
|
|
currentBroadcasters={masterLineData.currentBroadcastersUserIds}
|
|
tunedInUsers={masterLineData.tunedInMemberIds}
|
|
/>
|
|
)} */}
|
|
</>
|
|
);
|
|
}
|
|
|
|
type PeerMap = { [userId: string]: Peer };
|
|
|
|
function StreamRoom({
|
|
lineId,
|
|
tunedInUsers,
|
|
currentBroadcasters,
|
|
}: {
|
|
lineId: string;
|
|
tunedInUsers?: string[];
|
|
currentBroadcasters?: string[];
|
|
}) {
|
|
// ?could move this up the tree and pass it down? or set it in the header?
|
|
// local stream specifically for this stream room
|
|
const [localStream, setLocalStream] = useState<MediaStream>();
|
|
const userStreamTagRef = useRef<HTMLVideoElement>(null);
|
|
const { data: userDetails } = useGetUserDetails();
|
|
|
|
// local peer map of userIds to peers
|
|
const [userPeers, setUserPeers] = useState<PeerMap>({});
|
|
|
|
// ws listen to events of user disconnecting and such or rely on tunedin members prop
|
|
const { $ws } = useLineDataProvider();
|
|
|
|
// todo: get the right constraints based on user settings...audio, video, or none? header selections? make a decision
|
|
useEffect(() => {
|
|
if (userDetails) {
|
|
navigator.mediaDevices
|
|
.getUserMedia({
|
|
video: false,
|
|
audio: {
|
|
echoCancellation: true,
|
|
autoGainControl: true,
|
|
},
|
|
})
|
|
.then((userStream) => {
|
|
setLocalStream(userStream);
|
|
|
|
if (userStreamTagRef?.current)
|
|
userStreamTagRef.current.srcObject = userStream;
|
|
|
|
// alternative to dom element
|
|
// const audio = new Audio();
|
|
// audio.autoplay = true;
|
|
// audio.srcObject = userStream;
|
|
|
|
// !TEST STUFF : removing distortion with headphones in
|
|
// test distortion to go away on disabling audio
|
|
// setTimeout(() => {
|
|
// console.log("stopping audio stream ");
|
|
|
|
// userStream.getTracks().forEach((track) => {
|
|
// track.enabled = !track.enabled;
|
|
|
|
// track.stop();
|
|
// });
|
|
// }, 0);
|
|
|
|
// take the initial list of tunedInUsers without my own id
|
|
const everyOtherTunedUserId = tunedInUsers.filter(
|
|
(tunedUserId) => tunedUserId !== userDetails?.user._id.toString()
|
|
);
|
|
|
|
const localPeerConnections: PeerMap = {};
|
|
|
|
everyOtherTunedUserId.forEach((otherTunedInUserId) => {
|
|
// create local peer objects for them
|
|
const localPeerInitiator = new Peer({
|
|
initiator: true,
|
|
stream: userStream,
|
|
trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times
|
|
});
|
|
|
|
// set it for our map that we will iterate through to display streams in other child view
|
|
localPeerConnections[otherTunedInUserId] = localPeerInitiator;
|
|
|
|
// notify each one with specific signal
|
|
localPeerInitiator.on("signal", (signal) => {
|
|
$ws.emit(
|
|
ServerRequestChannels.RTC_CALL_REQUEST,
|
|
new RtcCallRequest(lineId, otherTunedInUserId, signal)
|
|
);
|
|
});
|
|
});
|
|
|
|
setUserPeers(localPeerConnections);
|
|
|
|
// answer calls
|
|
$ws.on(
|
|
`${ServerResponseChannels.RTC_NEW_USER_JOINED_RESPONSE_PREFIX}:${lineId}`,
|
|
(res: RtcNewUserResponse) => {
|
|
// create a local peer connection for this new user
|
|
|
|
console.log(
|
|
"ooo newbie joined room, I guess I will accept it and send him my signal"
|
|
);
|
|
console.log(res);
|
|
|
|
var peerForMeAndNewbie = new Peer({
|
|
initiator: false,
|
|
trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times
|
|
stream: userStream, // add in my own stream that I got before
|
|
});
|
|
|
|
peerForMeAndNewbie.on("signal", (signal) => {
|
|
console.log(
|
|
"as the answerer, I am going to send back my signal so that the newbie can update his local peer for me"
|
|
);
|
|
$ws.emit(
|
|
ServerRequestChannels.RTC_ANSWER_REQUEST,
|
|
new RtcAnswerRequest(lineId, res.newUserId, signal)
|
|
);
|
|
});
|
|
|
|
peerForMeAndNewbie.signal(res.simplePeerSignal);
|
|
|
|
setUserPeers((prevUsersPeers) => {
|
|
const newUserPeers = { ...prevUsersPeers };
|
|
|
|
newUserPeers[res.newUserId] = peerForMeAndNewbie;
|
|
|
|
return newUserPeers;
|
|
});
|
|
}
|
|
);
|
|
|
|
// take care of answers recevied
|
|
$ws.on(
|
|
`${ServerResponseChannels.RTC_RECEIVING_ANSWER_RESPONSE_PREFIX}:${lineId}`,
|
|
(res: RtcReceiveAnswerResponse) => {
|
|
console.log(
|
|
`oooo some master received my call and accepted it ${JSON.stringify(
|
|
res
|
|
)}`
|
|
);
|
|
console.log(res);
|
|
|
|
// find the peer we created earlier for this master
|
|
// ?is this okay? using the setter to get the current state?
|
|
setUserPeers((previousUserPeersMap) => {
|
|
const newUserPeerMap = { ...previousUserPeersMap };
|
|
|
|
console.log(
|
|
"here is the current peers map",
|
|
previousUserPeersMap
|
|
);
|
|
|
|
const peerForAnswerer = newUserPeerMap[res.answererUserId];
|
|
|
|
if (peerForAnswerer) {
|
|
peerForAnswerer.signal(res.simplePeerSignal);
|
|
} else {
|
|
console.error(
|
|
"could not find the peer we created before for this master"
|
|
);
|
|
}
|
|
|
|
// note needed to change state so that the peer object that gets iterated in dom isn't the old referenced one/we trigger refresh for the child component
|
|
return newUserPeerMap;
|
|
});
|
|
}
|
|
);
|
|
|
|
$ws.on(
|
|
`${ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE}:${lineId}`,
|
|
(res: SomeoneUntunedFromLineResponse) => {
|
|
// find the peer object and remove from our userPeers map
|
|
// will also cause unmounting the child component which destroys peer object but also can destroy here
|
|
}
|
|
);
|
|
})
|
|
.catch((error) => {
|
|
console.error(error);
|
|
|
|
toast.error(
|
|
"Make sure that you have permissions enabled and microphone connected"
|
|
);
|
|
});
|
|
}
|
|
|
|
// TODO: p1 : destroy peers on load
|
|
// ! remove ws handlers so that the same channels don't get triggered twice when I retune into this line
|
|
return () => {
|
|
$ws.removeListener(
|
|
`${ServerResponseChannels.RTC_RECEIVING_ANSWER_RESPONSE_PREFIX}:${lineId}`
|
|
);
|
|
$ws.removeListener(
|
|
`${ServerResponseChannels.RTC_NEW_USER_JOINED_RESPONSE_PREFIX}:${lineId}`
|
|
);
|
|
};
|
|
}, [userDetails, setUserPeers]);
|
|
|
|
// calculate diff to clean our userPeerMap to unmount and detroy certain peer connections
|
|
useEffect(() => {}, [tunedInUsers]);
|
|
|
|
useEffect(() => {
|
|
console.log("keeping an eye on user peers map");
|
|
console.log(userPeers);
|
|
}, [userPeers]);
|
|
|
|
// todo, someone tell the main object that I am finally connected after everything...different than tuned in
|
|
|
|
// TODO: p1...when the peer map user count > tunedIn.length, then we get rid of the right person from list cuzz they have officially left or disconnected
|
|
useEffect(() => {
|
|
console.log("change in tuned in users in the streaming room!!!");
|
|
|
|
setUserPeers((prevUserPeersMap) => {
|
|
// go through the userIds here
|
|
// if tunedIn users doesn't have a userId, this guy prolly disconnected
|
|
|
|
const newMap = { ...prevUserPeersMap }; //ensures going through the list of peers again to remove specific ones
|
|
|
|
const otherUserIdsPeers = Object.keys(prevUserPeersMap);
|
|
|
|
otherUserIdsPeers.forEach((otherUserId) => {
|
|
// problem if we are trying to show stream of someone who is not tuned in
|
|
if (!tunedInUsers.includes(otherUserId)) {
|
|
console.log("user left with id:", otherUserId);
|
|
const disconnectedLocalPeer = newMap[otherUserId];
|
|
|
|
if (disconnectedLocalPeer) {
|
|
disconnectedLocalPeer.peer?.destroy();
|
|
delete newMap[otherUserId];
|
|
}
|
|
}
|
|
});
|
|
|
|
return newMap;
|
|
});
|
|
}, [tunedInUsers]);
|
|
|
|
// todo: when user isUserBroadcasting is false, disable localUserStream in this component
|
|
// and also emit, which should already have been done
|
|
|
|
// we only loop through peers that are associated to user Ids which exist in the currentBroadcasters array
|
|
return (
|
|
<>
|
|
<audio autoPlay muted ref={userStreamTagRef} controls />
|
|
this is the video of people
|
|
{userPeers &&
|
|
Object.entries(userPeers).map(([userId, peer]) => (
|
|
<PeerStreamRenderer
|
|
key={userId}
|
|
isBroadcasting={currentBroadcasters?.includes(userId)}
|
|
peer={peer}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* take in the specified peer and whether or not he is broadcasting
|
|
*/
|
|
function PeerStreamRenderer({
|
|
peer,
|
|
isBroadcasting,
|
|
}: {
|
|
peer: Peer;
|
|
isBroadcasting: boolean;
|
|
}) {
|
|
const streamRef = useRef<HTMLVideoElement>(null);
|
|
|
|
useEffect(() => {
|
|
peer.on("stream", (remotePeerStream: MediaStream) => {
|
|
console.log(
|
|
"stream coming in from remote peer...BUT, only going to show once they broadcast"
|
|
);
|
|
|
|
if (streamRef?.current) streamRef.current.srcObject = remotePeerStream;
|
|
});
|
|
|
|
return () => peer.destroy();
|
|
}, [streamRef]);
|
|
|
|
// when someone is broadcasting, we enable their stream
|
|
|
|
return (
|
|
<>
|
|
<audio muted={!isBroadcasting} autoPlay controls ref={streamRef} />
|
|
</>
|
|
);
|
|
}
|