Files
nirvana-v3/packages/legacy/terminal/index.tsx
2022-05-12 15:24:36 -05:00

567 lines
19 KiB
TypeScript

import { $desktopMode, $selectedLineId } from "../../controller/recoil";
import { Avatar, Skeleton, Tooltip } from "antd";
import { FiActivity, FiHeadphones, FiSettings, FiSun } from "react-icons/fi";
import { GlobalHotKeys, KeyMap } from "react-hotkeys";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useGetUserDetails, useUserLines } from "../../controller/index";
import { useRecoilState, useSetRecoilState } from "recoil";
import { FaPlus } from "react-icons/fa";
import LineIcon from "../../components/lines/lineIcon/index";
import { LineMemberState } from "@nirvana/core/models/line.model";
import LineRow from "../../components/lines/lineRow.tsx/index";
import MasterLineData from "@nirvana/core/models/masterLineData.model";
import NewLineModal from "./newLine";
import toast from "react-hot-toast";
import { useLineDataProvider } from "../../controller/lineDataProvider";
/**
* Socket Provider
* Line Data Provider
*/
export default function NirvanaTerminal({
overlayOnly,
}: {
overlayOnly: boolean;
}) {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
const { data: userDetails } = useGetUserDetails();
const [selectedLineId, setSelectedLineId] = useRecoilState($selectedLineId);
const [desktopMode, setDesktopMode] = useRecoilState($desktopMode);
// simply using this query for specific data on loading
// todo: add these properties in context provider value although more work down the line for control
const { isLoading: isLoadingInitialLines } = useUserLines();
const { linesMap, handleTuneToLine, handleUnTuneToLine } =
useLineDataProvider();
useEffect(() => {
// console.log("change/update in lines map");
// console.log(linesMap);
}, [linesMap]);
// ! remounting when toggle tuning and untoggle tuning a line
// because it changes the list that it's in...solution 1: put everything in one list and use one sort to handle toggle tuned items to be clean
// the change in the object or the property which is specifically currentUserMember of the masterLineData sent in to line row
// causes a re-render but not a unmount and remount
const allLines: MasterLineData[] = useMemo(() => {
const masterLines: MasterLineData[] = Object.values(linesMap);
// TODO: sort based on the audio blocks and currentMember lastActiveDate
return masterLines.filter(
(masterLine) =>
masterLine.currentUserMember.state === LineMemberState.INBOX
);
}, [linesMap]);
const toggleTunedLines = useMemo(() => {
const masterLines: MasterLineData[] = Object.values(linesMap);
return masterLines.filter(
(masterLine) =>
masterLine.currentUserMember.state === LineMemberState.TUNED
);
}, [linesMap]);
const selectedLine: MasterLineData | undefined = useMemo(() => {
if (!selectedLineId) return undefined;
// find the line from the data provider
if (linesMap[selectedLineId]) {
// console.log("looking for selected Line in map for details section");
const foundSelectedLine = linesMap[selectedLineId];
// on mount of this, we want to temporarily tune into the line if we are not already tuned in...which would happen if we toggle tuned in
if (
!foundSelectedLine.tunedInMemberIds?.includes(
userDetails?.user?._id.toString()
)
) {
handleTuneToLine(selectedLineId, false);
}
return { ...foundSelectedLine };
}
return undefined;
}, [selectedLineId, linesMap]);
// todo: sort/order based on activity and activity date and currently broadcasting/live
const handleEscape = useCallback(() => {
console.log("deselecting line");
setSelectedLineId((prevSelectedLineId) => {
// ! only want to untune if it's a temporarily tuned line
if (selectedLine?.currentUserMember?.state === LineMemberState.INBOX)
handleUnTuneToLine(prevSelectedLineId);
return null;
});
}, [setSelectedLineId, selectedLine]);
/** show user line details on click of one line */
const handleSelectLine = useCallback(
(newLineIdToSelect: string) => {
if (newLineIdToSelect !== selectedLine?.lineDetails._id.toString()) {
setSelectedLineId((prevSelectedLineId) => {
// untune myself from the previously selected if it was a temporary one/inbox
// todo: p3: consolidate this logic as it's used in escape but different scenarios sort of so p3
if (
prevSelectedLineId &&
selectedLine.currentUserMember?.state === LineMemberState.INBOX
)
handleUnTuneToLine(prevSelectedLineId);
return newLineIdToSelect;
});
}
},
[setSelectedLineId, selectedLine]
);
const handleToggleTuneToLine = useCallback(
(lineId: string, turnToggleOn: boolean) => {
// inhibit if they are trying to turn on and already have 3 toggle tuned
if (toggleTunedLines?.length >= 3 && turnToggleOn) {
toast.error("You cannot toggle more than 3 lines!");
return;
}
handleTuneToLine(lineId, turnToggleOn);
},
[toggleTunedLines, handleTuneToLine]
);
const handleStartBroadcast = useCallback(
(lineId: string) => () => {
console.log(`starting broadcast for ${lineId}!!!`);
// todo: enable stream in this tuned in channel
// if (lineId) handleUserBroadcast(lineId, true);
},
[]
);
const handleStopBroadcast = useCallback(
(lineId: string) => () => {
console.log(`stopping broadcast for ${lineId}!!!`);
// todo: disable stream in this tuned in channel
// if (lineId) handleUserBroadcast(lineId, false);
},
[]
);
const keyMap: KeyMap = useMemo(
() => ({
DESELECT_LINE: "esc",
START_BROADCAST: {
sequence: "`",
action: "keydown",
},
STOP_BROADCAST: {
sequence: "`",
action: "keyup",
},
}),
[]
);
const handlers = useMemo(
() => ({
DESELECT_LINE: handleEscape,
START_BROADCAST: handleStartBroadcast(selectedLineId),
STOP_BROADCAST: handleStopBroadcast(selectedLineId),
}),
[selectedLineId]
);
return (
<>
<GlobalHotKeys handlers={handlers} keyMap={keyMap} allowChanges />
<div className="flex flex-row flex-1">
<div className="flex flex-col bg-white w-[400px] relative group">
{/* modal for creating new line */}
<NewLineModal
open={isModalVisible}
handleClose={() => setIsModalVisible(false)}
/>
{/* tuned in lines block */}
<div className="bg-gray-100 flex flex-col shadow-lg">
{/* tuned in header + general controls */}
<div className="flex flex-row items-center py-3 px-4 pb-0">
<span className="flex flex-row gap-2 items-center justify-start text-gray-400 animate-pulse">
<FiActivity className="text-sm" />
<h2 className="text-inherit text-sm">Tuned In</h2>
<p className="text-slate-300 text-xs">{`${
toggleTunedLines?.length || 0
}/3`}</p>
</span>
</div>
{/* list of toggle tuned lines */}
<div className="flex flex-col mt-2">
{toggleTunedLines.map((masterLineData) => (
<LineRow
key={`terminalListLines-${masterLineData.lineDetails._id.toString()}`}
masterLineData={masterLineData}
handleSelectLine={handleSelectLine}
/>
))}
</div>
</div>
{!(allLines.length > 0) && !(toggleTunedLines.length > 0) && (
<span className="text-gray-300 text-sm my-5 text-center">
You have no lines! <br /> Create one to connect to your team
instantly.
</span>
)}
{/* rest of the lines */}
<div className={"flex flex-col"}>
{isLoadingInitialLines ? (
<Skeleton />
) : (
allLines.map((masterLineData) => (
<LineRow
key={`terminalListLines-${masterLineData.lineDetails._id.toString()}`}
masterLineData={masterLineData}
handleSelectLine={handleSelectLine}
/>
))
)}
</div>
<div
onClick={() => setIsModalVisible(true)}
className="absolute bottom-3 right-3 z-10 scale-0
group-hover:scale-100 ease-in-out hover:transition group-hover:transition delay-100 duration-200"
>
<button
className="flex flex-row gap-2 items-center justify-evenly
shadow-xl bg-gray-800 p-2 text-white text-xs"
>
<FaPlus />
<span>New line</span>
</button>
</div>
</div>
{selectedLine ? (
<LineDetailsTerminal
handleToggleTuneToLine={handleToggleTuneToLine}
selectedLine={selectedLine}
/>
) : (
<div
className="flex flex-col flex-1 justify-center items-center bg-gray-100
border-l border-l-gray-200"
>
<span className="text-xl text-gray-800">
{`Hi ${userDetails?.user?.givenName}!`}
</span>
<span className="text-md text-gray-400">You're all set!</span>
</div>
)}
</div>
</>
);
}
function LineDetailsTerminal({
selectedLine,
handleToggleTuneToLine,
}: {
selectedLine: MasterLineData;
handleToggleTuneToLine: (lineId: string, turnToggleOn: boolean) => void;
}) {
const { data: userDetails } = useGetUserDetails();
console.log("selected line", selectedLine);
const isUserToggleTuned = useMemo(
() => selectedLine?.currentUserMember?.state === LineMemberState.TUNED,
[selectedLine]
);
// seeing if I am in the list of broadcasters
// the source of truth from the socket connections telling me if my clicking actually made a round trip
const isUserBroadcasting = useMemo(
() =>
selectedLine?.currentBroadcastersUserIds?.includes(
userDetails?.user._id.toString()
),
[userDetails, selectedLine]
);
// showing all tuned in members...they may not hear me since they might be doing something else
// but feeling of presentness
const tunedProfiles = useMemo(() => {
const pictureSources: { name: string; pictureSrc: string }[] = [];
selectedLine.tunedInMemberIds?.forEach((tunedInMemberUserId) => {
// don't want to see my own picture
// TODO: don't show myself!?
if (tunedInMemberUserId === userDetails.user._id.toString()) {
pictureSources.push();
pictureSources.push({
name: userDetails.user?.givenName,
pictureSrc: userDetails.user?.picture,
});
return;
}
const otherUserObject = selectedLine.otherUserObjects?.find(
(userObj) => userObj._id.toString() === tunedInMemberUserId
);
if (otherUserObject?.picture)
pictureSources.push({
name: otherUserObject.givenName,
pictureSrc: otherUserObject.picture,
});
});
return pictureSources;
}, [selectedLine, userDetails]);
// pics for the line icons
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);
selectedLine.otherUserObjects?.forEach((otherUser) => {
if (otherUser.picture) pictureSources.push(otherUser.picture);
});
return pictureSources;
}, [selectedLine, userDetails]);
return (
<div
className="flex flex-col flex-1 bg-gray-100
border-l border-l-gray-200 relative"
>
{/* line details */}
<div
className="p-4
flex flex-row items-center gap-2 justify-end border-b-gray-200 border-b"
>
{profilePictures && (
<LineIcon grayscale={false} sourceImages={profilePictures} />
)}
<div className="flex flex-col items-start mr-auto group">
<span className="flex flex-row gap-2 items-center">
<h2 className={`text-lg text-slate-800 font-semibold`}>
{selectedLine.lineDetails.name ||
selectedLine.otherUserObjects[0].givenName}
</h2>
<button
className={`p-1 hidden group-hover:flex justify-center items-center hover:bg-gray-300
transition-all hover:scale-105`}
>
<FiSettings className="text-gray-400 text-xs" />
</button>
</span>
<span className="flex flex-row gap-2 items-center">
<span className="text-gray-300 text-xs">{`${
selectedLine.otherMembers?.length + 1 ?? 0
} members`}</span>
<span className="h-1 w-1 bg-gray-800 rounded-full"></span>
<span className="text-teal-500 text-xs">{`${
selectedLine.tunedInMemberIds?.length ?? 0
} on the line`}</span>
</span>
</div>
{/* TODO: move to on hover of line row */}
<Tooltip
placement="left"
title={`${
isUserToggleTuned ? "click to untoggle" : "click to stay tuned in"
}`}
>
<button
className={`p-2 flex justify-center items-center shadow-lg
hover:scale-105 transition-all animate-pulse ${
isUserToggleTuned ? "bg-gray-800 text-white" : "text-black"
}`}
onClick={() =>
isUserToggleTuned
? handleToggleTuneToLine(
selectedLine.lineDetails._id.toString(),
false
)
: handleToggleTuneToLine(
selectedLine.lineDetails._id.toString(),
true
)
}
>
<FiActivity className="text-md" />
</button>
</Tooltip>
</div>
{/* line timeline */}
<div className="flex flex-col items-center gap-2 my-2 mx-auto max-w-lg w-full">
<span
className={"text-gray-300 text-sm cursor-pointer hover:underline"}
>
load more
</span>
<span className={"text-gray-300 text-sm"}>yesterday</span>
<div className={"rounded border border-gray-200 flex flex-col w-full"}>
{selectedLine.otherUserObjects.map((otherUser) => (
<div
className="flex flex-row p-2 gap-2 items-center bg-transparent
border-b border-b-gray-200 last:border-b-0"
>
<Avatar
key={`linehistory-${1}`}
src={otherUser.picture}
shape="square"
size={"default"}
// grayscale if not playing?
className={`${true && "grayscale"}`}
/>
<span className="text-gray-500">{otherUser.givenName}</span>
<span className="ml-auto text-xs text-gray-300">{`${
Math.floor(Math.random() * 10) + 1
}:${Math.floor(Math.random() * 100) + 10}pm |`}</span>
<span className="text-gray-400 text-md">{`${
Math.floor(Math.random() * 60) + 1
} seconds`}</span>
</div>
))}
<div
className="flex flex-row p-2 gap-2 items-center bg-transparent
border-b border-b-gray-200 last:border-b-0"
>
<Avatar
key={`linehistory-${1}`}
src={userDetails.user.picture}
shape="square"
size={"default"}
// grayscale if not playing?
className={`${true && "grayscale"}`}
/>
<span className="text-gray-500">{"Arjun Patel"}</span>
<span className="ml-auto text-xs text-gray-300">{`${
Math.floor(Math.random() * 10) + 1
}:${Math.floor(Math.random() * 100) + 10}pm |`}</span>
<span className="text-gray-400 text-md">{`${
Math.floor(Math.random() * 60) + 1
} seconds`}</span>
</div>
</div>
<span className={"text-gray-300 text-sm"}>today</span>
<div
className={
"rounded border border-gray-400 flex flex-col w-full shadow-xl"
}
>
{selectedLine.otherUserObjects.map((otherUser) => (
// TODO: show the shadow if it's unheard
<div
className="flex flex-row p-2 gap-2 items-center bg-transparent
border-b border-b-gray-400 last:border-b-0"
>
<Avatar
key={`linehistory-${1}`}
src={otherUser.picture}
shape="square"
size={"default"}
// grayscale if not playing?
className={`${false && "grayscale"}`}
/>
<span className="text-gray-600 font-semibold">
{otherUser.name}
</span>
<span className="ml-auto text-xs text-gray-400">{`${
Math.floor(Math.random() * 10) + 1
}:${Math.floor(Math.random() * 100) + 10}pm |`}</span>
<span className="text-gray-500 text-md">{`${
Math.floor(Math.random() * 60) + 1
} seconds`}</span>
</div>
))}
</div>
{/* live broadcasters */}
<span
className={"flex flex-row gap-2 items-center text-teal-500 text-sm"}
>
<FiSun className="animate-ping" />
<span>right now</span>
</span>
<div
className={
"rounded border border-teal-500 flex flex-col w-full shadow-2xl"
}
>
{selectedLine.otherUserObjects.map((otherUser) => (
<div className="flex flex-row p-2 gap-2 items-center bg-transparent">
<Avatar
key={`linehistory-${1}`}
src={otherUser.picture}
shape="square"
size={"large"}
// grayscale if not playing?
className={`shadow-lg`}
/>
<span className="text-gray-600 font-semibold">
{otherUser.name}
</span>
<Tooltip title={"audio only"}>
<FiHeadphones className="text-gray-600 text-md ml-auto" />
</Tooltip>
</div>
))}
</div>
</div>
<button
className={`p-3 absolute right-3 bottom-3 flex justify-center items-center shadow-2xl
hover:scale-105 transition-all ${
isUserBroadcasting
? "bg-teal-800 text-white"
: "text-teal-800 border-teal-800 border"
}`}
>
<FiSun className="text-lg" />
</button>
</div>
);
}