throwing legacy trash components
This commit is contained in:
@@ -1,108 +0,0 @@
|
||||
import Channels, { STORE_ITEMS } from "../../electron/constants";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
|
||||
import { $jwtToken } from "../../controller/recoil";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import Logo from "../../components/Logo";
|
||||
import { useLogin } from "../../controller/index";
|
||||
|
||||
export default function Login() {
|
||||
const { mutateAsync } = useLogin();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const setJwtToken = useSetRecoilState($jwtToken);
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI.once(
|
||||
Channels.GOOGLE_AUTH_TOKENS,
|
||||
async (tokens: {
|
||||
access_token: string;
|
||||
id_token: string;
|
||||
refresh_token: string;
|
||||
}) => {
|
||||
console.log("got tokens", tokens);
|
||||
|
||||
setIsLoading(true);
|
||||
// todo: implement refresh token procedure in api layer by sending refresh_token and such
|
||||
|
||||
const loginResponse = await mutateAsync({
|
||||
accessToken: tokens.access_token,
|
||||
idToken: tokens.id_token,
|
||||
});
|
||||
|
||||
const { jwtToken, userDetails } = loginResponse;
|
||||
setJwtToken(jwtToken);
|
||||
}
|
||||
);
|
||||
|
||||
// todo: figure out how to clean up with the preload api
|
||||
// return () => {
|
||||
// window.electronAPI.removeAllListeners(Channels.GOOGLE_AUTH_TOKENS);
|
||||
// };
|
||||
}, []);
|
||||
|
||||
const continueAuth = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
// send to main process
|
||||
window.electronAPI.auth.initiateLogin();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col space-y-5 justify-center items-center
|
||||
h-screen w-screen
|
||||
bg-zinc-700"
|
||||
>
|
||||
<Logo className="scale-50" />
|
||||
|
||||
{/* ! TESTING PURPOSES */}
|
||||
<div className={"text-white flex flex-col gap-5"}>
|
||||
<button
|
||||
onClick={() =>
|
||||
setJwtToken(
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2MjczYTNjZGVmYzc3MDNhZDg1NWYzMjYiLCJnb29nbGVVc2VySWQiOiIxMTM0NzA3ODY2OTAzNTMxMDkwODYiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2ptMjlTZDFXbm04TnZaYlhrb3N2ZjZTb0lENmtCUDVPSFJMVklPQlE9czk2LWMiLCJlbWFpbCI6InBhdGVsLmFyanVuNTBAZ21haWwuY29tIiwibmFtZSI6IkFyanVuIFBhdGVsIiwiaWF0IjoxNjUxOTY2MDg2fQ.bGK4DaUyuHCIRFhX5g3xQxI5SVMKR7hmte1UpmTZaVc"
|
||||
)
|
||||
}
|
||||
>
|
||||
Personal Account
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
setJwtToken(
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2MjczYTZmYmVmYzc3MDNhZDg1NWYzMjciLCJnb29nbGVVc2VySWQiOiIxMTE2NzEzNTI4MDkwMTg3NjM4MjYiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2dUUmFNaVVUZkNaX1ZOM2M3SFJyYUlZUmpmN1BNSUZjVThJdlZiPXM5Ni1jIiwiZW1haWwiOiJhcmp1bnBhdGVsQGJlcmtlbGV5LmVkdSIsIm5hbWUiOiJBcmp1biBQYXRlbCIsImlhdCI6MTY1MTk2NjA5OH0.53TbuXaHDTivEpqVr-TP5A1vVIFznT3q_HoVxOReZKc"
|
||||
)
|
||||
}
|
||||
>
|
||||
Berkeley Email
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
setJwtToken(
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2MjczZTAxNTM1OTg3YWZhMGZjOTA5NzYiLCJnb29nbGVVc2VySWQiOiIxMTQyMTgxMjM0Mzk1OTA4OTU4MjAiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL29ndy9BRGVhNEk0RUhmY08ycVc1blEtelJXdzdRdTVOMWVQdnU5cHkyQzFPbUVxbj1zNjQtYy1tbyIsImVtYWlsIjoidXNlbmlydmFuYUBnbWFpbC5jb20iLCJuYW1lIjoiTmlydmFuYSBTdXBwb3J0IiwiaWF0IjoxNjUxOTY3MDA2fQ.9AgmMW2LYv4QNqibvopiKAaV0GWNbChujWiY6t0OZeQ"
|
||||
)
|
||||
}
|
||||
>
|
||||
Nirvana Support
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span className="text-white">Attempting to log you in</span>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={continueAuth}
|
||||
className=" text-md text-zinc-200 py-2 px-5 border border-gray-200 transition-all hover:bg-gray-200 hover:text-teal-500 rounded flex flex-row items-center space-x-2"
|
||||
>
|
||||
<FcGoogle className="text-lg" />
|
||||
<span>Continue with Google</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
export type LogoType = "primary" | "small";
|
||||
|
||||
export default function Logo({
|
||||
className,
|
||||
type,
|
||||
grayscale = false,
|
||||
}: {
|
||||
className?: string;
|
||||
type?: LogoType;
|
||||
grayscale?: boolean;
|
||||
}) {
|
||||
if (type === "small") {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="31.75"
|
||||
viewBox="0 0 85 107"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`${className} ${grayscale ? "text-black grayscale" : ""}`}
|
||||
>
|
||||
<path
|
||||
d="M0.398438 25.9375C1.84375 26.9531 2.89453 27.6562 3.91016 28.4023C11.9258 34.2617 19.9336 40.125 27.9414 45.9961C32.8008 49.5391 37.1211 53.5898 40.1445 58.8867C42.6094 63.0898 44.1328 67.6172 44.7148 72.4648C44.9258 74.1797 45.0469 75.9062 45.0703 77.6367C45.1055 86.8281 45.0703 96.0156 45.0703 105.207V106.809C44.5742 106.562 44.0938 106.289 43.625 105.992C34.1094 99.043 24.5391 92.1523 15.1055 85.0859C6.83203 78.8672 1.97266 70.5273 0.734375 60.1484C0.527344 58.3633 0.421875 56.5703 0.414062 54.7734C0.398439 46.0312 0.398438 37.2891 0.398438 28.5508V25.9375Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M84.2539 15.5039V23.0859C84.2539 28.0352 84.2851 32.9844 84.2539 37.9297C84.1875 46.7539 80.4922 53.875 73.6367 59.1641C66.6484 64.5703 59.4023 69.6328 52.2617 74.8477C52.0078 75.0352 51.7422 75.207 51.2695 75.5352C51.164 75.0273 51.0937 74.5117 51.0586 73.9922C51.0586 66.9883 50.9492 59.9805 51.0781 52.9805C51.2382 44.5977 54.539 37.5352 61.0937 32.3828C68.3945 26.6445 76.0117 21.3125 83.4961 15.8125C83.7422 15.6914 83.9961 15.5859 84.2539 15.5039Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M44.957 44.9219L39.5781 40.9766C36.0156 38.3672 32.4336 35.7812 28.8828 33.1484C23.1992 28.9219 20.3281 23.2539 20.332 16.1367V2.45703C20.332 1.82813 20.332 1.20312 20.332 0.308594C20.8125 0.488281 21.2773 0.707031 21.7188 0.964844C26.6875 4.58203 31.6875 8.16016 36.5898 11.8672C41.1133 15.2852 44 19.7812 44.8516 25.4844C44.9648 26.0469 45.0352 26.6172 45.0625 27.1953C45.082 32.6562 45.0938 38.1172 45.0898 43.5742C45.0898 43.8828 45.0312 44.1914 44.957 44.9219Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="233"
|
||||
height="162"
|
||||
viewBox="0 0 233 162"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`${className} ${grayscale ? "text-black grayscale" : ""}`}
|
||||
>
|
||||
<path
|
||||
d="M74.3984 26.6133C75.8437 27.6289 76.8945 28.332 77.9102 29.0781C85.9258 34.9375 93.9336 40.8008 101.941 46.6719C106.801 50.2148 111.121 54.2656 114.145 59.5625C116.609 63.7656 118.133 68.293 118.715 73.1406C118.926 74.8555 119.047 76.582 119.07 78.3125C119.105 87.5039 119.07 96.6914 119.07 105.883V107.484C118.574 107.238 118.094 106.965 117.625 106.668C108.109 99.7188 98.5391 92.8281 89.1055 85.7617C80.832 79.543 75.9727 71.2031 74.7344 60.8242C74.5273 59.0391 74.4219 57.2461 74.4141 55.4492C74.3984 46.707 74.3984 37.9648 74.3984 29.2266V26.6133Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M158.254 16.1797V23.7617C158.254 28.7109 158.285 33.6602 158.254 38.6055C158.188 47.4297 154.492 54.5508 147.637 59.8398C140.649 65.2461 133.402 70.3086 126.262 75.5234C126.008 75.7109 125.742 75.8828 125.27 76.2109C125.164 75.7031 125.094 75.1875 125.059 74.668C125.059 67.6641 124.949 60.6562 125.078 53.6562C125.238 45.2734 128.539 38.2109 135.094 33.0586C142.395 27.3203 150.012 21.9883 157.496 16.4883C157.742 16.3672 157.996 16.2617 158.254 16.1797Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M118.957 45.5977L113.578 41.6523C110.016 39.043 106.434 36.457 102.883 33.8242C97.1992 29.5977 94.3281 23.9297 94.332 16.8125V3.13281C94.332 2.50391 94.332 1.87891 94.332 0.984375C94.8125 1.16406 95.2774 1.38281 95.7188 1.64062C100.688 5.25781 105.688 8.83594 110.59 12.543C115.113 15.9609 118 20.457 118.852 26.1602C118.965 26.7227 119.035 27.293 119.062 27.8711C119.082 33.332 119.094 38.793 119.09 44.25C119.09 44.5586 119.031 44.8672 118.957 45.5977Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M11.3774 127.295V132.295C13.522 128.724 16.9478 126.935 21.6587 126.935C25.5532 126.935 28.7095 128.24 31.1274 130.842C33.5415 133.435 34.7524 136.967 34.7524 141.435V161.279H24.4087V142.826C24.4087 140.638 23.8228 138.947 22.6587 137.748C21.5024 136.552 19.9087 135.951 17.8774 135.951C15.854 135.951 14.2681 136.552 13.1118 137.748C11.9556 138.947 11.3774 140.638 11.3774 142.826V161.279H0.955566V127.295H11.3774Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M67.0403 127.295V133.638C69.4661 129.17 72.7122 126.935 76.7747 126.935V137.529H74.2278C71.8293 137.529 70.0325 138.099 68.8372 139.232C67.6379 140.369 67.0403 142.357 67.0403 145.201V161.279H56.6184V127.295H67.0403Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M77.8701 127.295H88.9482L96.6826 152.576L104.417 127.295H115.495L103.433 161.279H89.917L77.8701 127.295Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M138.751 150.513C140.251 149.045 141.001 146.974 141.001 144.295C141.001 141.619 140.251 139.56 138.751 138.123C137.251 136.677 135.524 135.951 133.579 135.951C131.63 135.951 129.907 136.681 128.407 138.138C126.907 139.599 126.157 141.67 126.157 144.357C126.157 147.037 126.907 149.099 128.407 150.545C129.907 151.982 131.63 152.701 133.579 152.701C135.524 152.701 137.251 151.974 138.751 150.513ZM120.048 156.842C117.048 153.592 115.548 149.431 115.548 144.357C115.548 139.287 117.048 135.115 120.048 131.842C123.055 128.572 126.723 126.935 131.048 126.935C135.368 126.935 138.688 128.541 141.001 131.748V127.295H151.423V161.279H141.001V156.342C138.489 159.916 135.118 161.701 130.891 161.701C126.673 161.701 123.055 160.084 120.048 156.842Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M168.718 127.295V132.295C170.863 128.724 174.289 126.935 179 126.935C182.894 126.935 186.05 128.24 188.468 130.842C190.882 133.435 192.093 136.967 192.093 141.435V161.279H181.75V142.826C181.75 140.638 181.164 138.947 180 137.748C178.843 136.552 177.25 135.951 175.218 135.951C173.195 135.951 171.609 136.552 170.453 137.748C169.296 138.947 168.718 140.638 168.718 142.826V161.279H158.296V127.295H168.718Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M219.37 150.513C220.87 149.045 221.62 146.974 221.62 144.295C221.62 141.619 220.87 139.56 219.37 138.123C217.87 136.677 216.143 135.951 214.198 135.951C212.249 135.951 210.526 136.681 209.026 138.138C207.526 139.599 206.776 141.67 206.776 144.357C206.776 147.037 207.526 149.099 209.026 150.545C210.526 151.982 212.249 152.701 214.198 152.701C216.143 152.701 217.87 151.974 219.37 150.513ZM200.667 156.842C197.667 153.592 196.167 149.431 196.167 144.357C196.167 139.287 197.667 135.115 200.667 131.842C203.674 128.572 207.342 126.935 211.667 126.935C215.987 126.935 219.307 128.541 221.62 131.748V127.295H232.042V161.279H221.62V156.342C219.108 159.916 215.737 161.701 211.51 161.701C207.292 161.701 203.674 160.084 200.667 156.842Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M55.172 105.619C56.454 104.173 58.666 104.039 60.1127 105.321C61.5594 106.603 61.6929 108.815 60.4109 110.262L50.5603 121.378C49.2783 122.825 47.0663 122.958 45.6196 121.676C44.1729 120.394 44.0394 118.182 45.3214 116.736L55.172 105.619Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
<path
|
||||
d="M40.077 126.98L50 127.002L49.923 160.874L40 160.852L40.077 126.98Z"
|
||||
fill="#438E86"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { useAuthCheck, useServerCheck } from "../../controller/index";
|
||||
|
||||
import { $jwtToken } from "../../controller/recoil";
|
||||
import Login from "../../pages/Login";
|
||||
import NirvanaApi from "../../controller/nirvanaApi";
|
||||
import { STORE_ITEMS } from "../../electron/constants";
|
||||
import SkeletonLoader from "../loading/skeleton";
|
||||
import { useEffect } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
|
||||
export default function ProtectedRoute({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const [jwtToken, setJwtToken] = useRecoilState($jwtToken);
|
||||
|
||||
const {
|
||||
isError: serverFailure,
|
||||
isLoading: serverLoading,
|
||||
isSuccess: isServerHealthy,
|
||||
error: serverStatusError,
|
||||
isFetching: serverFetching,
|
||||
} = useServerCheck();
|
||||
|
||||
const { isLoading, isError, isFetching, isSuccess, refetch } = useAuthCheck(
|
||||
!serverLoading
|
||||
);
|
||||
|
||||
// ! REMOVING FOR TESTING MULTIPLE CLIENTS
|
||||
// useEffect(() => {
|
||||
// // on load of this, if we already have jwt tokens in store,
|
||||
// // then try using them with auth check
|
||||
// window.electronAPI.store
|
||||
// .get(STORE_ITEMS.AUTH_SESSION_JWT)
|
||||
// .then((jwtToken: string) => {
|
||||
// if (jwtToken) {
|
||||
// NirvanaApi._jwtToken = jwtToken;
|
||||
// setJwtToken(jwtToken);
|
||||
// refetch();
|
||||
|
||||
// console.log("retrieved jwtToken from storage", jwtToken);
|
||||
// } else {
|
||||
// console.log("no jwt token in store");
|
||||
// }
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("change in jwt token", jwtToken);
|
||||
|
||||
if (jwtToken) {
|
||||
window.electronAPI.store.set(STORE_ITEMS.AUTH_SESSION_JWT, jwtToken);
|
||||
setJwtToken(jwtToken);
|
||||
} else {
|
||||
window.electronAPI.store.set(STORE_ITEMS.AUTH_SESSION_JWT, null);
|
||||
}
|
||||
|
||||
NirvanaApi._jwtToken = jwtToken;
|
||||
|
||||
refetch();
|
||||
}, [jwtToken]);
|
||||
|
||||
// first time loading
|
||||
if (serverLoading)
|
||||
return (
|
||||
<span className="text-md text-gray-400 flex items-center justify-center flex-1 text-center p-5 h-screen">
|
||||
Sorry...this is our bad. Our servers are loading. We are trying our best
|
||||
to back up and running! :) <br /> Please contact me for urgent concerns:
|
||||
arjunpatel@berkeley.edu
|
||||
</span>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container h-screen w-screen flex flex-col justify-center mx-10">
|
||||
<SkeletonLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
// if we can successfully get user details, we are good to continue
|
||||
// basic auth...any additional auth should be done at lower levels
|
||||
if (isSuccess) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return <Login />;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Avatar, Badge } from "antd";
|
||||
|
||||
import { UserOutlined } from "@ant-design/icons";
|
||||
|
||||
const AvatarWithBadge = ({ src }: { src: string }) => (
|
||||
<>
|
||||
<span>
|
||||
<Badge dot>
|
||||
<Avatar shape="square" src={src} />
|
||||
</Badge>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
export default AvatarWithBadge;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Avatar, Tooltip } from "antd";
|
||||
|
||||
import { FaAngleRight } from "react-icons/fa";
|
||||
import { ReactElement } from "react";
|
||||
import { User } from "@nirvana/core/models/user.model";
|
||||
|
||||
export default function BasicUserRow({
|
||||
user,
|
||||
rightJsx,
|
||||
}: {
|
||||
user: User;
|
||||
rightJsx?: ReactElement;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
key={user._id.toString() + new Date().toDateString()}
|
||||
className="border-t border-t-gray-200 py-3 px-2 flex flex-row justify-start
|
||||
items-center w-full hover:bg-gray-200 cursor-pointer group"
|
||||
>
|
||||
<Avatar
|
||||
key={`searchUsers-${user._id.toString()}`}
|
||||
shape="square"
|
||||
src={user.picture}
|
||||
size="default"
|
||||
className="shadow-lg mr-2"
|
||||
/>
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-gray-500 text-lg font-semibold">{user.name}</span>
|
||||
|
||||
<span className="text-gray-300">{user.email}</span>
|
||||
</span>
|
||||
|
||||
<div className="ml-auto">{rightJsx}</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { User, UserStatus } from "@nirvana/core/models";
|
||||
|
||||
export default function UserAvatarWithStatus(props: { user: User }) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={props.user.picture}
|
||||
className="rounded-lg h-8 hover:bg-zinc-200 hover:cursor-pointer hover:scale-110"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<UserStatusBubble status={props.user.status} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserStatusBubble(props: { status: UserStatus }) {
|
||||
switch (props.status) {
|
||||
case UserStatus.ONLINE:
|
||||
return (
|
||||
<span className="absolute -bottom-1 -right-1 rounded-full bg-emerald-600 h-3 w-3"></span>
|
||||
);
|
||||
case UserStatus.OFFLINE:
|
||||
return (
|
||||
<span className="absolute -bottom-1 -right-1 rounded-full bg-zinc-400 h-3 w-3"></span>
|
||||
);
|
||||
case UserStatus.FLOW_STATE:
|
||||
return (
|
||||
<span className="absolute -bottom-1 -right-1 rounded-full bg-pink-400 h-3 w-3"></span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span className="absolute -bottom-1 -right-1 rounded-full bg-pink-400 h-3 w-3"></span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { UserStatus } from "@nirvana/core/models";
|
||||
|
||||
export default function UserStatusText(props: { status: UserStatus }) {
|
||||
switch (props.status) {
|
||||
case UserStatus.ONLINE:
|
||||
return <span className="text-emerald-600 text-sm lowercase">ONLINE</span>;
|
||||
case UserStatus.OFFLINE:
|
||||
return <></>;
|
||||
case UserStatus.FLOW_STATE:
|
||||
return (
|
||||
<span className="text-pink-400 text-sm lowercase">FLOW STATE</span>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import {
|
||||
$desktopMode,
|
||||
$jwtToken,
|
||||
$mediaSettings,
|
||||
} from "../../controller/recoil";
|
||||
import { Avatar, Dropdown, Menu, Tooltip } from "antd";
|
||||
import { FiCheck, FiLogOut, FiMic, FiMicOff } from "react-icons/fi";
|
||||
import { GlobalHotKeys, KeyMap } from "react-hotkeys";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
|
||||
|
||||
import { FaSearch } from "react-icons/fa";
|
||||
import HorizontalLogo from "../../../../components/logo/horizontal";
|
||||
import Logo from "../Logo";
|
||||
import { LogoType } from "../../../../components/logo/full";
|
||||
import { useGetUserDetails } from "../../controller/index";
|
||||
|
||||
/**
|
||||
* TODO: add in video mode
|
||||
*
|
||||
*/
|
||||
export default function NirvanaHeader({
|
||||
onHeaderFocus,
|
||||
}: {
|
||||
onHeaderFocus: () => void;
|
||||
}) {
|
||||
const { data: userDetailsRes, isLoading, isError } = useGetUserDetails();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchInput, setSearchInput] = useState<string>("");
|
||||
|
||||
const [desktopMode, setDesktopMode] = useRecoilState($desktopMode);
|
||||
|
||||
const [mediaSettings, setMediaSettings] = useRecoilState($mediaSettings);
|
||||
|
||||
const setJwtToken = useSetRecoilState($jwtToken);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) setJwtToken(null);
|
||||
}, [isError]);
|
||||
|
||||
/** hide the search bar in the header so that it's cleaner for these two modes */
|
||||
const shouldHideSearch = useMemo(() => {
|
||||
if (desktopMode === "flowState" || desktopMode === "overlayOnly")
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}, [desktopMode]);
|
||||
|
||||
const selectSearch = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const onSearchChange = (e) => {
|
||||
setSearchInput(e.target.value);
|
||||
};
|
||||
|
||||
const keyMap: KeyMap = {
|
||||
SELECT_SEARCH: {
|
||||
name: "select search to start searching",
|
||||
sequence: "/",
|
||||
action: "keyup",
|
||||
},
|
||||
};
|
||||
const handlers = {
|
||||
SELECT_SEARCH: selectSearch,
|
||||
};
|
||||
|
||||
const handleMuteToggle = useCallback(() => {
|
||||
setMediaSettings((previousMediaSettings) => ({
|
||||
...previousMediaSettings,
|
||||
isMuted: !previousMediaSettings.isMuted,
|
||||
}));
|
||||
}, [setMediaSettings]);
|
||||
|
||||
const handleSignOut = useCallback(() => {
|
||||
setJwtToken(null);
|
||||
}, [setJwtToken]);
|
||||
|
||||
if (isLoading) return <>loading</>;
|
||||
|
||||
// todo: do I need a mute mode? isn't that just flow state
|
||||
// might confuse user overall
|
||||
const profileMenu = (
|
||||
<Menu
|
||||
items={[
|
||||
// {
|
||||
// label: (
|
||||
// <span onClick={handleMuteToggle}>
|
||||
// {mediaSettings.isMuted ? "Unmute" : "Mute"}
|
||||
// </span>
|
||||
// ),
|
||||
// icon: <> {mediaSettings.isMuted ? <FiMicOff /> : <FiMic />} </>,
|
||||
// key: `profile-menu-${1}`,
|
||||
// },
|
||||
{
|
||||
label: <span>Audio Only</span>,
|
||||
icon: <> {mediaSettings.mode === "audio" ? <FiCheck /> : <></>} </>,
|
||||
disabled: false,
|
||||
key: `profile-menu-${2}`,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Tooltip title="coming soon">
|
||||
<span>Video</span>
|
||||
</Tooltip>
|
||||
),
|
||||
icon: <>{mediaSettings.mode === "video" ? <FiCheck /> : <></>} </>,
|
||||
disabled: true,
|
||||
key: `profile-menu-${3}`,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Tooltip title="coming soon">
|
||||
<span>Screen</span>
|
||||
</Tooltip>
|
||||
),
|
||||
icon: <>{mediaSettings.mode === "screen" ? <FiCheck /> : <></>} </>,
|
||||
disabled: true,
|
||||
key: `profile-menu-${3}`,
|
||||
},
|
||||
|
||||
{
|
||||
type: "divider",
|
||||
key: `profile-menu-${4}`,
|
||||
},
|
||||
{
|
||||
label: <span onClick={handleSignOut}>Sign Out</span>,
|
||||
icon: <FiLogOut />,
|
||||
key: `profile-menu-${5}`,
|
||||
danger: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalHotKeys handlers={handlers} keyMap={keyMap} allowChanges />
|
||||
|
||||
<div
|
||||
className="flex flex-row items-center bg-gray-100 p-4 border-b border-b-gray-200"
|
||||
id="titlebar"
|
||||
>
|
||||
<Tooltip
|
||||
title={desktopMode === "flowState" ? "unplugged" : "connected"}
|
||||
placement="right"
|
||||
>
|
||||
<div onClick={onHeaderFocus}>
|
||||
<Logo type="small" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{!shouldHideSearch && (
|
||||
<div className="mx-auto flex flex-row items-center space-x-2">
|
||||
<FaSearch className="text-xs text-gray-300" />
|
||||
<input
|
||||
placeholder="Type / to search"
|
||||
className="bg-transparent placeholder-gray-300 placeholder:text-xs focus:outline-none"
|
||||
ref={inputRef}
|
||||
onChange={onSearchChange}
|
||||
value={searchInput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* todo: move this ghost button to components */}
|
||||
{desktopMode === "flowState" ? (
|
||||
<button
|
||||
onClick={() => setDesktopMode("terminal")}
|
||||
className="ml-auto text-gray-300 text-xs p-3 transition-all hover:bg-gray-200"
|
||||
>
|
||||
connect
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setDesktopMode("flowState")}
|
||||
className="text-gray-300 text-xs p-3 transition-all hover:bg-gray-200"
|
||||
>
|
||||
flow state
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Dropdown overlay={profileMenu}>
|
||||
<div className={"cursor-pointer ml-2"}>
|
||||
{userDetailsRes?.user?.picture &&
|
||||
mediaSettings.mode === "audio" && (
|
||||
<Avatar
|
||||
key={`userHeaderProfilePicture`}
|
||||
className="shadow-md hover:scale-110 transition-all"
|
||||
size={"large"}
|
||||
alt={userDetailsRes?.user?.givenName}
|
||||
src={userDetailsRes?.user?.picture}
|
||||
shape="square"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
{/* menu for the output options */}
|
||||
{/* <Menu
|
||||
open={menuOpen}
|
||||
id="user-output-selection-menu"
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleCloseMenu}
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "basic-button",
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={() => setOutputMode("audio")}>
|
||||
<ListItemIcon>
|
||||
<HeadsetMicSharp fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Audio Only</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => setOutputMode("video")}>
|
||||
<ListItemIcon>
|
||||
<VideocamSharp fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Video</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Avatar } from "antd";
|
||||
import React from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
function LineIcon({
|
||||
sourceImages,
|
||||
grayscale = true,
|
||||
}: {
|
||||
sourceImages: string[];
|
||||
grayscale: boolean;
|
||||
}) {
|
||||
const isMultiple = useMemo(() => {
|
||||
if (sourceImages?.length > 1) return true;
|
||||
|
||||
return false;
|
||||
}, [sourceImages]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMultiple ? (
|
||||
<div
|
||||
className={`relative ${
|
||||
grayscale ? "grayscale" : ""
|
||||
} h-[32px] w-[32px]`}
|
||||
>
|
||||
{sourceImages.map((avatarSrc, index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<Avatar
|
||||
key={`lineIcon-${avatarSrc}-${index}`}
|
||||
src={avatarSrc}
|
||||
shape="square"
|
||||
size={"small"}
|
||||
className={`shadow-lg absolute bottom-0 left-0 bg-slate-200`}
|
||||
/>
|
||||
);
|
||||
} else if (index === 1) {
|
||||
return (
|
||||
<Avatar
|
||||
key={`lineIcon-${avatarSrc}-${index}`}
|
||||
src={avatarSrc}
|
||||
shape="square"
|
||||
size={"small"}
|
||||
className={`absolute top-0 right-0 bg-slate-200`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: add third in the frame if we want later
|
||||
return <></>;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
sourceImages?.map((avatarSrc, index) => (
|
||||
<Avatar
|
||||
key={`lineIcon-${avatarSrc}-${index}`}
|
||||
src={avatarSrc}
|
||||
shape="square"
|
||||
size={"default"}
|
||||
className={`${grayscale && "grayscale"} shadow-lg`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(LineIcon);
|
||||
@@ -1,424 +0,0 @@
|
||||
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 { 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 '@nirvana/core/sockets/channels';
|
||||
import moment from 'moment';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import useAuth from '../../../../desktop/src/providers/AuthProvider';
|
||||
|
||||
// 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 { user } = useAuth();
|
||||
|
||||
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 there is someone or me broadcasting here
|
||||
if (masterLineData.currentBroadcastersUserIds?.length > 0)
|
||||
return <FiSun className="text-teal-500 animate-pulse" />;
|
||||
|
||||
if (isUserTunedIn) return <FiActivity className="text-black animate-pulse" />;
|
||||
|
||||
// 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 (false)
|
||||
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 rounded ${
|
||||
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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Skeleton } from "antd";
|
||||
|
||||
export default function SkeletonLoader() {
|
||||
return <Skeleton avatar paragraph={{ rows: 4 }} />;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import NirvanaApi, { ApiCalls } from "./nirvanaApi";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
|
||||
import { $jwtToken } from "./recoil";
|
||||
import { queryClient } from "../pages/nirvanaApp";
|
||||
import { useRecoilValue } from "recoil";
|
||||
|
||||
// ====== QUERIES
|
||||
|
||||
/**
|
||||
* ensure that the server is up
|
||||
*/
|
||||
export function useServerCheck() {
|
||||
return useQuery("SERVER_CHECK", ApiCalls.serverCheck, {
|
||||
retry: true,
|
||||
refetchOnWindowFocus: false,
|
||||
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAuthCheck(enabled: boolean = true) {
|
||||
const jwtToken = useRecoilValue($jwtToken);
|
||||
|
||||
return useQuery("AUTH_CHECK", ApiCalls.authCheck, {
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
|
||||
refetchInterval: 10000,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLogin() {
|
||||
return useMutation("LOGIN", ApiCalls.login, {});
|
||||
}
|
||||
|
||||
export function useGetUserDetails() {
|
||||
return useQuery("USER_DETAILS", ApiCalls.getUserDetails, {
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: (err) => {
|
||||
console.log(err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUserSearch(searchQuery: string) {
|
||||
return useQuery("USER_SEARCH", () => ApiCalls.userSearch(searchQuery), {
|
||||
enabled: searchQuery ? true : false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** query responsible solely for getting lines and the intersection with sockets happens elsewhere */
|
||||
export function useUserLines() {
|
||||
// todo: base/source of truth for getting all of the lines for the user
|
||||
// merge with sockets + audio clip data + master data + convomember data
|
||||
|
||||
return useQuery("USER_LINES", ApiCalls.getUserLines, {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchIntervalInBackground: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
// =========== MUTATIONS
|
||||
export function useGetDmByUserId() {
|
||||
return useMutation(ApiCalls.getDmByUserId);
|
||||
}
|
||||
|
||||
export function useCreateLine() {
|
||||
return useMutation(ApiCalls.createLine, {
|
||||
onSuccess: (res, req) => {
|
||||
queryClient.invalidateQueries(["USER_LINES"]);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,446 +0,0 @@
|
||||
import { $desktopMode, $jwtToken, $selectedLineId } from "./recoil";
|
||||
import {
|
||||
ConnectToLineRequest,
|
||||
ServerRequestChannels,
|
||||
ServerResponseChannels,
|
||||
SomeoneConnectedResponse,
|
||||
SomeoneTunedResponse,
|
||||
SomeoneUntunedFromLineResponse,
|
||||
StartBroadcastingRequest,
|
||||
StopBroadcastingRequest,
|
||||
TuneToLineRequest,
|
||||
UntuneFromLineRequest,
|
||||
UserStartedBroadcastingResponse,
|
||||
UserStoppedBroadcastingResponse,
|
||||
} from "@nirvana/core/sockets/channels";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Socket, io } from "socket.io-client";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||
|
||||
import { LineMemberState } from "@nirvana/core/models/line.model";
|
||||
import MasterLineData from "@nirvana/core/models/masterLineData.model";
|
||||
import { User } from "@nirvana/core/models";
|
||||
import { queryClient } from "../pages/nirvanaApp";
|
||||
import toast from "react-hot-toast";
|
||||
import { useUserLines } from "./index";
|
||||
|
||||
let $ws: Socket;
|
||||
|
||||
function useSocketHandler(linesData: MasterLineData[]) {
|
||||
const jwtToken = useRecoilValue($jwtToken);
|
||||
|
||||
const [linesMap, setLinesMap] = useState<LineIdToMasterLine>({});
|
||||
const setDesktopMode = useSetRecoilState($desktopMode);
|
||||
|
||||
/**
|
||||
* handle ws connection
|
||||
*/
|
||||
|
||||
// ! POTENTIAL DIAGNOSIS with socket reconnections on electron idle causing
|
||||
// no more event listeners being fired off: potentially because the listeners were for an older manager or instance
|
||||
// so when the client reconnects, with another instance, the old listeners are lost
|
||||
// https://stackoverflow.com/questions/34984980/socket-io-not-receiving-emit-data-after-reconnect
|
||||
// https://socket.io/docs/v4/client-options/#reconnection
|
||||
|
||||
useEffect(() => {
|
||||
$ws = io("http://localhost:5000", {
|
||||
query: { token: jwtToken },
|
||||
transports: ["websocket"],
|
||||
upgrade: false,
|
||||
forceNew: true,
|
||||
reconnection: false, // ! TESTING THE PROBLEM WITH RECONNECTION CLIENT LISTENERS NOT ACTIVATING
|
||||
});
|
||||
|
||||
$ws.on("connect", () => {
|
||||
console.log(
|
||||
"SOCKETS | CLIENT CONNECTED in socket handler, setting up client side listeners for requests"
|
||||
);
|
||||
toast.success("you are connected");
|
||||
|
||||
/**
|
||||
* initiate listeners
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO: P0 : notified that someone else added me to a line they created
|
||||
*/
|
||||
// 1. get the new line from axios or just from the ws itself
|
||||
// 2. add it to the lines map with all of the right data
|
||||
// 3. make sure to connect to it as if it's inbox material
|
||||
|
||||
// when me or anyone just initially connects to line
|
||||
$ws.on(
|
||||
ServerResponseChannels.SOMEONE_CONNECTED_TO_LINE,
|
||||
(res: SomeoneConnectedResponse) => {
|
||||
console.log(
|
||||
`connected to line...here are all of the updated in the conected line ${res.lineId}...this isn't reliable considering it's not updated later`,
|
||||
res.allConnectedIntoUserIds
|
||||
);
|
||||
|
||||
setLinesMap((prevLinesMap) => {
|
||||
const newMap = { ...prevLinesMap };
|
||||
|
||||
if (newMap[res.lineId])
|
||||
newMap[res.lineId].connectedMemberIds = [
|
||||
...(newMap[res.lineId]?.connectedMemberIds ?? []),
|
||||
res.userId,
|
||||
];
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// someone tuning in, including perhaps me | either toggled in or just temporary
|
||||
$ws.on(
|
||||
ServerResponseChannels.SOMEONE_TUNED_INTO_LINE,
|
||||
(res: SomeoneTunedResponse) => {
|
||||
console.log(
|
||||
`here are all of updated users in the tuned in room`,
|
||||
res.allTunedIntoUserIds
|
||||
);
|
||||
|
||||
// TODO: if toggled in, make sure to update the current line member in the lines map so that
|
||||
// we can know to untune if user selects another line
|
||||
|
||||
// below, we are setting the list of tuned in folks based on fresh list from the server
|
||||
// better than just adding and removing? i think so, but have to handle not interrupting existing peer connections as this changes
|
||||
setLinesMap((prevLinesMap) => {
|
||||
const newMap = { ...prevLinesMap };
|
||||
|
||||
if (newMap[res.lineId]) {
|
||||
newMap[res.lineId].tunedInMemberIds = res.allTunedIntoUserIds;
|
||||
|
||||
// if user is me, make sure to show me my updated line member association
|
||||
if (
|
||||
newMap[res.lineId].currentUserMember?.userId.toString() ===
|
||||
res.userId
|
||||
) {
|
||||
newMap[res.lineId].currentUserMember.lastVisitDate = new Date();
|
||||
newMap[res.lineId].currentUserMember.state = res.toggledIn
|
||||
? LineMemberState.TUNED
|
||||
: LineMemberState.INBOX;
|
||||
}
|
||||
|
||||
// TODO: update the relevant lineMember (based on which userId is given): state and last visit date if current user is joining
|
||||
// and not just if it's the current user toggling in
|
||||
// right now, we just want user to know number of folks tuned and no need to expose who is toggle tuned...that lineMember can be stale
|
||||
}
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
$ws.on(
|
||||
ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE,
|
||||
(res: SomeoneUntunedFromLineResponse) => {
|
||||
console.log(
|
||||
`here are all of updated users in the tuned in room`,
|
||||
res.allTunedIntoUserIds
|
||||
);
|
||||
|
||||
// TODO: if toggled in, make sure to update the current line member in the lines map so that
|
||||
// we can know to untune if user selects another line
|
||||
|
||||
setLinesMap((prevLinesMap) => {
|
||||
const newMap = { ...prevLinesMap };
|
||||
|
||||
// TODO: update the relevant lineMember (based on which userId is given): state and last visit date if current user is joining
|
||||
|
||||
if (newMap[res.lineId])
|
||||
newMap[res.lineId].tunedInMemberIds = res.allTunedIntoUserIds;
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
$ws.on(
|
||||
ServerResponseChannels.SOMEONE_STARTED_BROADCASTING,
|
||||
(res: UserStartedBroadcastingResponse) => {
|
||||
console.log("someone is starting to broadcast");
|
||||
|
||||
setLinesMap((prevLinesMap) => {
|
||||
const newMap = { ...prevLinesMap };
|
||||
|
||||
if (newMap[res.lineId])
|
||||
newMap[res.lineId].currentBroadcastersUserIds = [
|
||||
...(newMap[res.lineId].currentBroadcastersUserIds ?? []),
|
||||
res.userId,
|
||||
];
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
$ws.on(
|
||||
ServerResponseChannels.SOMEONE_STOPPED_BROADCASTING,
|
||||
(res: UserStoppedBroadcastingResponse) => {
|
||||
setLinesMap((prevLinesMap) => {
|
||||
const newMap = { ...prevLinesMap };
|
||||
|
||||
if (newMap[res.lineId]?.currentBroadcastersUserIds) {
|
||||
newMap[res.lineId].currentBroadcastersUserIds = newMap[
|
||||
res.lineId
|
||||
].currentBroadcastersUserIds.filter(
|
||||
(broadcasterUserId) => broadcasterUserId !== res.userId
|
||||
);
|
||||
}
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// on disconnections, just switch it to flow state?
|
||||
// what if there was another problem?
|
||||
$ws.io.on("close", () => {
|
||||
console.error(
|
||||
"SOCKET | there was a problem with your app...connection closed likely due to idling or manual disconnect"
|
||||
);
|
||||
// todo: figure out the right thing based on the situation whether it's a problem or user unplugs
|
||||
// toast(
|
||||
// "Disconnected due to idling or some other issue. Please reconnect or refresh to fix the problem."
|
||||
// );
|
||||
|
||||
toast("unplugging");
|
||||
setDesktopMode("flowState");
|
||||
});
|
||||
|
||||
// client-side errors
|
||||
$ws.on("connect_error", (err) => {
|
||||
console.error(`SOCKETS | ${err.message}`); // prints the message associated with the error
|
||||
toast.error("sorry...this is our bad...please refresh with cmd + r");
|
||||
|
||||
// force refetch of server status as well as these generally go hand in hand
|
||||
|
||||
// this should overall remount this component currently which is what we want for new data
|
||||
queryClient.invalidateQueries("SERVER_CHECK");
|
||||
});
|
||||
|
||||
// on unmounting this component, we want to disconnect
|
||||
return () => {
|
||||
$ws.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** handle initial data coming in and creating the initial line map
|
||||
* and doing initial connections/tune ins ?could trigger this later?
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (linesData?.length > 0) {
|
||||
setLinesMap((prevMappings) => {
|
||||
// go through the lines from the persistent store
|
||||
|
||||
// get all of the id's and map assign to the main object
|
||||
|
||||
// ?prolly have no previous at this point...but I'm okay with override since this
|
||||
// ?useeffect is triggered on the refetching of the persistent store so we
|
||||
// ?are prolly looking to do a full app refresh and connection refresh
|
||||
const newMap = { ...prevMappings };
|
||||
|
||||
linesData.forEach((masterLine) => {
|
||||
const lineId = masterLine.lineDetails._id.toString();
|
||||
newMap[lineId] = masterLine;
|
||||
|
||||
handleConnectToLine(lineId);
|
||||
|
||||
// tune into lines that I should be
|
||||
if (masterLine.currentUserMember.state === LineMemberState.TUNED) {
|
||||
// don't need to turn toggle on, it's already on
|
||||
// ! CHANGE PARADIGM TO NOT INCLUDING TOGGLING IN REQUESTS WITH THE SAME AS TUNING IN
|
||||
// right now, writing just because of later logic
|
||||
handleTuneToLine(lineId, true);
|
||||
}
|
||||
});
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
}, [linesData, setLinesMap]);
|
||||
|
||||
/** handlers for emitting events to server */
|
||||
const handleConnectToLine = useCallback(
|
||||
(lineId: string) => {
|
||||
$ws.emit(
|
||||
ServerRequestChannels.CONNECT_TO_LINE,
|
||||
new ConnectToLineRequest(lineId)
|
||||
);
|
||||
},
|
||||
[$ws]
|
||||
);
|
||||
|
||||
/**
|
||||
* toggle into a specific line
|
||||
* @param temporary: denotes whether we are just listening in or want to persist "toggling" it on so that it shows up in overlay
|
||||
* TODO: have loading state for this particular part of context value
|
||||
*/
|
||||
const handleTuneToLine = useCallback(
|
||||
(lineId: string, turnToggleOn: boolean = false) => {
|
||||
// they already are in the socket room for updates including media connections and disconnections
|
||||
// but set the flag so that the line row can know whether or not to start the webrtc process
|
||||
// and know when to get out or disconnect from the webrtc when the flag turns off
|
||||
|
||||
$ws.emit(
|
||||
ServerRequestChannels.TUNE_INTO_LINE,
|
||||
new TuneToLineRequest(lineId, turnToggleOn)
|
||||
);
|
||||
},
|
||||
[$ws]
|
||||
);
|
||||
|
||||
const handleUnTuneToLine = useCallback(
|
||||
(lineId: string) => {
|
||||
// they already are in the socket room for updates including media connections and disconnections
|
||||
// but set the flag so that the line row can know whether or not to start the webrtc process
|
||||
// and know when to get out or disconnect from the webrtc when the flag turns off
|
||||
|
||||
$ws.emit(
|
||||
ServerRequestChannels.UNTUNE_FROM_LINE,
|
||||
new UntuneFromLineRequest(lineId)
|
||||
);
|
||||
},
|
||||
[$ws]
|
||||
);
|
||||
|
||||
/**
|
||||
* This is when the user wants to tell everyone that they are streaming/broadcasting/buzzing to
|
||||
* a specific line, whether they are toggle tuned or temporarily tuned in
|
||||
*
|
||||
* Note: They could be using push to talk or toggle broadcast
|
||||
*
|
||||
* Note: this is handling the data transmission as is the responsibility of this overall context provider
|
||||
* and not necessarily the aspect of enabling and disabling streaming as webrtc is reliable enough for that
|
||||
*
|
||||
* @param lineId the line that the current user is talking into
|
||||
*/
|
||||
const handleStartBroadcast = useCallback(
|
||||
(lineId: string) => {
|
||||
// emit telling people
|
||||
$ws.emit(
|
||||
ServerRequestChannels.BROADCAST_TO_LINE,
|
||||
new StartBroadcastingRequest(lineId)
|
||||
);
|
||||
},
|
||||
[$ws]
|
||||
);
|
||||
|
||||
const handleStopBroadcast = useCallback(
|
||||
(lineId: string) => {
|
||||
// emit telling people
|
||||
$ws.emit(
|
||||
ServerRequestChannels.STOP_BROADCAST_TO_LINE,
|
||||
new StopBroadcastingRequest(lineId)
|
||||
);
|
||||
|
||||
// ?handle recording here as well? or record the incoming stream instead?
|
||||
// ?could just take in the audiochunks here and all, but not sure yet
|
||||
},
|
||||
[$ws]
|
||||
);
|
||||
|
||||
/**
|
||||
* get more audio blocks for a certain line
|
||||
*/
|
||||
const handleFetchMoreAudioBlocks = useCallback(
|
||||
(lineId: string) => {
|
||||
// simple axios fetch for this specific block
|
||||
// update the specific line in lines map
|
||||
// set the state to trigger the re-renders in the tree
|
||||
},
|
||||
[$ws]
|
||||
);
|
||||
|
||||
/**
|
||||
* TODO: handle telling overall current client that we are finally rtc connected for a certain line
|
||||
*/
|
||||
|
||||
// TODO: move all of these emitters to just having child views doing the work here
|
||||
return {
|
||||
linesMap,
|
||||
handleConnectToLine,
|
||||
handleTuneToLine,
|
||||
handleStartBroadcast,
|
||||
handleStopBroadcast,
|
||||
handleFetchMoreAudioBlocks,
|
||||
handleUnTuneToLine,
|
||||
};
|
||||
}
|
||||
|
||||
type LineIdToMasterLine = {
|
||||
[lineId: string]: MasterLineData;
|
||||
};
|
||||
interface ILineDataContext {
|
||||
// represents the modified and up to date lines data with availability from session
|
||||
linesMap: LineIdToMasterLine;
|
||||
relevantUsers: User[];
|
||||
|
||||
// in case some components need this like in the peer handling
|
||||
$ws: Socket;
|
||||
}
|
||||
|
||||
const LineDataContext = React.createContext<
|
||||
Partial<ILineDataContext & ReturnType<typeof useSocketHandler>>
|
||||
>({
|
||||
linesMap: {},
|
||||
relevantUsers: [],
|
||||
$ws: undefined,
|
||||
});
|
||||
|
||||
export function LineDataProvider({ children }) {
|
||||
// TODO: have internal loading to prevent showing even router and all if something is not ready
|
||||
// or have it within each property within the context value
|
||||
|
||||
// persistent store of lines
|
||||
// ?just do simple synchronous axios/fetch in useEffect and manage isLoading ourselves?
|
||||
const { data: basicUserLinesData } = useUserLines();
|
||||
const desktopMode = useRecoilValue($desktopMode);
|
||||
|
||||
// ! passing in the same data of the query passes reference so changes that happen in socketHandler impact react query cache
|
||||
const { linesMap, ...handlers } = useSocketHandler(
|
||||
basicUserLinesData?.data?.masterLines
|
||||
);
|
||||
|
||||
// TODO: not complete...not sure of priority of this
|
||||
// if we go into flow state
|
||||
// emit flow state message to all of my rooms
|
||||
// persist it in my user object
|
||||
// and then disconnect
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (desktopMode === "flowState" && $ws) {
|
||||
console.log(
|
||||
"SOCKETS | telling all of my connected rooms that I am unplugging"
|
||||
);
|
||||
|
||||
$ws.emit(ServerRequestChannels.GOING_INTO_FLOW_STATE);
|
||||
}
|
||||
};
|
||||
}, [desktopMode, $ws]);
|
||||
|
||||
// avoid children rendering if they don't have the ws to make individual calls with
|
||||
if (!$ws) {
|
||||
return <span>attempting to connect you for the here and now...</span>;
|
||||
}
|
||||
|
||||
const value: ILineDataContext & ReturnType<typeof useSocketHandler> = {
|
||||
linesMap, // TODO send the updated map instead of this array
|
||||
relevantUsers: [],
|
||||
$ws,
|
||||
...handlers,
|
||||
};
|
||||
|
||||
return (
|
||||
<LineDataContext.Provider value={value}>
|
||||
{children}
|
||||
</LineDataContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLineDataProvider() {
|
||||
return useContext(LineDataContext);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse, Method } from "axios";
|
||||
|
||||
import CreateLineRequest from "@nirvana/core/requests/createLine.request";
|
||||
import GetUserLinesResponse from "@nirvana/core/responses/getUserLines.response";
|
||||
import { Line } from "@nirvana/core/models/line.model";
|
||||
import LoginResponse from "../../../core/responses/login.response";
|
||||
import MasterLineData from "@nirvana/core/models/masterLineData.model";
|
||||
import NirvanaResponse from "../../../core/responses/nirvanaResponse";
|
||||
import { User } from "@nirvana/core/models";
|
||||
import UserDetailsResponse from "../../../core/responses/userDetails.response";
|
||||
import UserSearchResponse from "../../../core/responses/userSearch.response";
|
||||
|
||||
// export const localHost = process.env.REACT_APP_API_DOMAIN;
|
||||
|
||||
export const localHost = "http://localhost:5000/api";
|
||||
|
||||
export default class NirvanaApi {
|
||||
// auth token from google that our backend will use
|
||||
static _jwtToken?: string;
|
||||
|
||||
static async fetch<T>(
|
||||
url: string,
|
||||
method: Method,
|
||||
privateRoute = false,
|
||||
body: object = null
|
||||
) {
|
||||
// use the auth token if it's a private route
|
||||
// error if no auth token and it's a private route
|
||||
// throw error and show message on anything that is an error from the backend
|
||||
|
||||
const fullUrl = localHost + url;
|
||||
|
||||
let res;
|
||||
if (privateRoute && !this._jwtToken) throw Error("No jwt token available!");
|
||||
|
||||
if (privateRoute && this._jwtToken) {
|
||||
res = await fetch(fullUrl, {
|
||||
method: method,
|
||||
headers: {
|
||||
Authorization: this._jwtToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body ? JSON.stringify(body) : null,
|
||||
});
|
||||
} else {
|
||||
res = await fetch(fullUrl);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) throw new Error("You are not authorized here");
|
||||
|
||||
throw new Error("Something went wrong");
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
}
|
||||
|
||||
async function serverCheck(): Promise<void> {
|
||||
return await NirvanaApi.fetch(`/status`, "GET", false);
|
||||
}
|
||||
|
||||
async function login(reqLoginTokens: {
|
||||
accessToken: string;
|
||||
idToken: string;
|
||||
}): Promise<LoginResponse> {
|
||||
return await NirvanaApi.fetch(
|
||||
`/user/login?access_token=${reqLoginTokens.accessToken}&id_token=${reqLoginTokens.idToken}`,
|
||||
"GET",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
async function authCheck(): Promise<void> {
|
||||
return await NirvanaApi.fetch(`/user/authcheck`, "GET", true);
|
||||
}
|
||||
|
||||
async function getUserDetails(): Promise<UserDetailsResponse> {
|
||||
return await NirvanaApi.fetch(`/user`, "GET", true);
|
||||
}
|
||||
|
||||
async function userSearch(searchQuery: string): Promise<UserSearchResponse> {
|
||||
return await NirvanaApi.fetch(
|
||||
`/search/users?query=${searchQuery}`,
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
async function getUserLines(): Promise<NirvanaResponse<GetUserLinesResponse>> {
|
||||
return await NirvanaApi.fetch<NirvanaResponse<GetUserLinesResponse>>(
|
||||
`/lines`,
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
async function getDmByUserId(otherUserId: string): Promise<Line> {
|
||||
return await NirvanaApi.fetch(`/lines/dm/${otherUserId}`, "GET", true);
|
||||
}
|
||||
|
||||
async function createLine(
|
||||
request: CreateLineRequest
|
||||
): Promise<NirvanaResponse<Line>> {
|
||||
return await NirvanaApi.fetch(`/lines`, "POST", true, request);
|
||||
}
|
||||
|
||||
export const ApiCalls = {
|
||||
serverCheck,
|
||||
login,
|
||||
authCheck,
|
||||
getUserDetails,
|
||||
userSearch,
|
||||
getUserLines,
|
||||
getDmByUserId,
|
||||
createLine,
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { atom } from "recoil";
|
||||
|
||||
export const $searchQuery = atom<string>({
|
||||
key: "SEARCH_QUERY",
|
||||
default: "",
|
||||
});
|
||||
|
||||
export const $jwtToken = atom<string>({
|
||||
key: "JWT_TOKEN",
|
||||
default: null,
|
||||
});
|
||||
|
||||
// conversation id
|
||||
export const $selectedConversation = atom<string>({
|
||||
key: "SELECTED_CONVERSATION",
|
||||
default: null,
|
||||
});
|
||||
|
||||
// new convo page trigger
|
||||
export const $newConvoPage = atom<boolean>({
|
||||
key: "NEW_CONVO_PAGE",
|
||||
default: false,
|
||||
});
|
||||
|
||||
// number of active lines
|
||||
export const $numberActiveLines = atom<number>({
|
||||
key: "NUMBER_ACTIVE_LINES",
|
||||
default: 0,
|
||||
});
|
||||
|
||||
// max number of active streams
|
||||
export const $maxNumberActiveStreams = atom<number>({
|
||||
key: "MAX_NUMBER_ACTIVE_STREAMS",
|
||||
default: 0,
|
||||
});
|
||||
|
||||
// ============
|
||||
|
||||
type DesktopMode = "flowState" | "overlayOnly" | "terminal";
|
||||
|
||||
export const $desktopMode = atom<DesktopMode>({
|
||||
key: "DESKTOP_MODE",
|
||||
default: "terminal",
|
||||
});
|
||||
|
||||
export const $selectedLineId = atom<string>({
|
||||
key: "SELECTED_LINE_ID",
|
||||
default: null,
|
||||
});
|
||||
|
||||
interface MediaSettings {
|
||||
mode: "audio" | "video" | "screen";
|
||||
isMuted: boolean;
|
||||
}
|
||||
export const $mediaSettings = atom<MediaSettings>({
|
||||
key: "MEDIA_SETTINGS",
|
||||
default: { isMuted: false, mode: "audio" },
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
|
||||
import NirvanaRouter from "./router";
|
||||
import ProtectedRoute from "../components/ProtectedRoute";
|
||||
import { ReactQueryDevtools } from "react-query/devtools";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { configure } from "react-hotkeys";
|
||||
import { io } from "socket.io-client";
|
||||
import testConnection from "@nirvana/core";
|
||||
|
||||
testConnection();
|
||||
|
||||
configure({
|
||||
// TODO: put back to default from docs: https://github.com/greena13/react-hotkeys#Configuration
|
||||
|
||||
/**
|
||||
* The HTML tags that React HotKeys should ignore key events from. This only works
|
||||
* if you are using the default ignoreEventsCondition function.
|
||||
* @type {String[]}
|
||||
*/
|
||||
ignoreTags: [],
|
||||
|
||||
/**
|
||||
* The function used to determine whether a key event should be ignored by React
|
||||
* Hotkeys. By default, keyboard events originating elements with a tag name in
|
||||
* ignoreTags, or a isContentEditable property of true, are ignored.
|
||||
*
|
||||
* @type {Function<KeyboardEvent>}
|
||||
*/
|
||||
// ignoreEventsCondition: function,
|
||||
});
|
||||
|
||||
// Create a client
|
||||
export const queryClient = new QueryClient();
|
||||
|
||||
function NirvanaApp() {
|
||||
return (
|
||||
<>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RecoilRoot>
|
||||
<ProtectedRoute>
|
||||
<NirvanaRouter />
|
||||
</ProtectedRoute>
|
||||
|
||||
<ReactQueryDevtools initialIsOpen={true} position={"bottom-left"} />
|
||||
</RecoilRoot>
|
||||
|
||||
<Toaster
|
||||
position={"bottom-right"}
|
||||
toastOptions={{
|
||||
style: {
|
||||
borderRadius: "10px",
|
||||
background: "#333",
|
||||
color: "#fff",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NirvanaApp;
|
||||
@@ -1,135 +0,0 @@
|
||||
import {
|
||||
$desktopMode,
|
||||
$maxNumberActiveStreams,
|
||||
$numberActiveLines,
|
||||
$selectedLineId,
|
||||
} from '../../controller/recoil';
|
||||
import Channels, { DEFAULT_APP_PRESET, Dimensions } from '../../electron/constants';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import FullMottoLogo from '../../../../components/logo/fullMotto';
|
||||
import { LineDataProvider } from '../../controller/lineDataProvider';
|
||||
import { LogoType } from '@nirvana/components/logo/full';
|
||||
import NirvanaHeader from '../../components/header/index';
|
||||
import NirvanaTerminal from '../terminal';
|
||||
|
||||
export default function NirvanaRouter() {
|
||||
const [selectedLineId, setSelectedLineId] = useRecoilState($selectedLineId);
|
||||
const [desktopMode, setDesktopMode] = useRecoilState($desktopMode);
|
||||
const numberOfOverlayColumns = useRecoilValue($numberActiveLines);
|
||||
const numberOfOverlayRows = useRecoilValue($maxNumberActiveStreams);
|
||||
|
||||
// handle all window resizing logic
|
||||
useEffect(() => {
|
||||
// add dimensions if it's not overlay only mode
|
||||
let finalDimensions: Dimensions = { height: 0, width: 0 };
|
||||
let finalPosition: 'center' | 'topRight';
|
||||
|
||||
const setAlwaysOnTop = desktopMode === 'overlayOnly';
|
||||
|
||||
// go hunting for which dimensions to have
|
||||
if (desktopMode === 'terminal' || desktopMode == 'flowState') {
|
||||
finalDimensions = DEFAULT_APP_PRESET;
|
||||
}
|
||||
|
||||
if (desktopMode === 'overlayOnly') {
|
||||
finalDimensions = {
|
||||
height: 68 + numberOfOverlayRows * 200,
|
||||
width: 360 + 360 * numberOfOverlayColumns,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(
|
||||
'setting new dimensions',
|
||||
desktopMode,
|
||||
numberOfOverlayColumns,
|
||||
numberOfOverlayRows,
|
||||
finalDimensions,
|
||||
setAlwaysOnTop,
|
||||
);
|
||||
|
||||
// send the final dimensions to main process
|
||||
window.electronAPI.window.resizeWindow({
|
||||
setAlwaysOnTop,
|
||||
dimensions: {
|
||||
height: finalDimensions.height,
|
||||
width: finalDimensions.width,
|
||||
},
|
||||
setPosition: finalPosition,
|
||||
addDimensions: false,
|
||||
});
|
||||
}, [desktopMode, numberOfOverlayColumns, numberOfOverlayRows]);
|
||||
|
||||
// on window blur, put app in overlay only mode
|
||||
// get rid of selected line from terminal if we had any...
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI.on(Channels.ON_WINDOW_BLUR, () => {
|
||||
console.log(
|
||||
'window blurring now, should be always on top and then ill tell main process to change dimensions',
|
||||
);
|
||||
// TODO: testing mode... uncomment both instructions below
|
||||
// setDesktopMode("overlayOnly");
|
||||
|
||||
// todo: make sure that if I am toggle broadcasted into a line, then don't deselect selected line
|
||||
// the overlay should be showing selected line if I am broadcasting toggled into it as well as of course all other toggle tuned ones
|
||||
// setSelectedLineId(null);
|
||||
});
|
||||
}, [setDesktopMode, setSelectedLineId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1">
|
||||
<NirvanaHeader onHeaderFocus={() => setDesktopMode('terminal')} />
|
||||
|
||||
{desktopMode === 'flowState' && <FlowState />}
|
||||
|
||||
{/* remount nirvana terminal */}
|
||||
{(desktopMode === 'terminal' || desktopMode === 'overlayOnly') && (
|
||||
<LineDataProvider>
|
||||
<NirvanaTerminal overlayOnly={desktopMode === 'overlayOnly'} />
|
||||
</LineDataProvider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Quote = {
|
||||
content: string;
|
||||
author: string;
|
||||
length: number;
|
||||
dateAdded: Date;
|
||||
_id: string;
|
||||
tags: string[];
|
||||
};
|
||||
function FlowState() {
|
||||
const [quote, setQuote] = useState<Quote>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://api.quotable.io/random')
|
||||
.then((res) => res.json())
|
||||
.then((data: Quote) => {
|
||||
console.warn(data);
|
||||
|
||||
setQuote(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
// do nothing, don't bother user, just don't show the quote
|
||||
console.warn(error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 justify-center items-center relative">
|
||||
{/* <img src="https://source.unsplash.com/random/?nature" /> */}
|
||||
<FullMottoLogo type={LogoType.small} className={'absolute bottom-2 mx-auto'} />
|
||||
|
||||
{quote && (
|
||||
<span className="flex flex-col justify-center items-center max-w-screen-sm">
|
||||
<span className="text-xl text-gray-800 font-semibold text-center">"{quote.content}"</span>
|
||||
<span className="tex-md italic text-gray-400">{quote.author}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,513 +0,0 @@
|
||||
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 React, { 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, userDetails]);
|
||||
|
||||
// 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 */}
|
||||
|
||||
<Tooltip placement="right" title={'These are your active rooms...'}>
|
||||
<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">Rooms</h2>
|
||||
|
||||
<p className="text-slate-300 text-xs">{`${toggleTunedLines?.length || 0}/3`}</p>
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{/* 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
|
||||
} in this room`}</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>
|
||||
);
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import { Avatar, Modal } from "antd";
|
||||
import { HotKeys, KeyMap } from "react-hotkeys";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCreateLine, useUserSearch } from "../../../controller/index";
|
||||
|
||||
import BasicUserRow from "../../../components/User/basicUserDetailsRow";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { User } from "@nirvana/core/models";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export default function NewLineModal({
|
||||
open,
|
||||
handleClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
}) {
|
||||
const [selectedPeople, setSelectedPeople] = useState<User[]>([]);
|
||||
|
||||
const [peopleSearchValue, setPeopleSearchValue] = useState<string>("");
|
||||
|
||||
// the actual state that goes to query...the debounced one so to speak
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const { refetch, data: searchRes } = useUserSearch(searchQuery);
|
||||
|
||||
const { mutateAsync, isLoading } = useCreateLine();
|
||||
|
||||
const [lineName, setLineName] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery) refetch();
|
||||
}, [searchQuery]);
|
||||
|
||||
const onSearch = useCallback(() => {
|
||||
console.log("enter key pressed");
|
||||
|
||||
if (peopleSearchValue) {
|
||||
console.log("searching for people in database");
|
||||
setSearchQuery(peopleSearchValue);
|
||||
}
|
||||
}, [setSearchQuery, peopleSearchValue]);
|
||||
|
||||
const selectUser = useCallback(
|
||||
(userToAdd: User) => {
|
||||
setSelectedPeople((prevSelectedUsers) => {
|
||||
// if user is not already in the selected user
|
||||
const foundUser = prevSelectedUsers.find(
|
||||
(currentUser) =>
|
||||
currentUser._id.toString() === userToAdd._id.toString()
|
||||
);
|
||||
if (!foundUser) {
|
||||
return [...prevSelectedUsers, userToAdd];
|
||||
}
|
||||
|
||||
toast.error("you already selected this person below!");
|
||||
return prevSelectedUsers;
|
||||
});
|
||||
},
|
||||
[setSelectedPeople]
|
||||
);
|
||||
|
||||
const unSelectUser = useCallback(
|
||||
(userIdToRemove: string) => {
|
||||
setSelectedPeople((prevSelectedUsers) => {
|
||||
return prevSelectedUsers.filter(
|
||||
(prevUser) => prevUser._id.toString() !== userIdToRemove
|
||||
);
|
||||
});
|
||||
},
|
||||
[setSelectedPeople]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
// TODO: prevent creating one-on-one line if already exists with x person?
|
||||
// ensure that we don't have a one on one chat already with x person if it's one person selected
|
||||
|
||||
// upon success,
|
||||
// make sure that the list of lines updates for this client and others so that it shows this new line
|
||||
// select the line so that it shows up in the line details for this client
|
||||
|
||||
console.log("trying to create line now!");
|
||||
|
||||
try {
|
||||
if (!selectedPeople?.length) {
|
||||
toast.error("you must select at least one person");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedMemberIds = selectedPeople.map((selectedPerson) =>
|
||||
selectedPerson._id.toString()
|
||||
);
|
||||
|
||||
const res = await mutateAsync({
|
||||
lineName,
|
||||
otherMemberIds: selectedMemberIds,
|
||||
});
|
||||
|
||||
toast.success("created line!");
|
||||
|
||||
// handle close once the new line is created
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
toast.error(error);
|
||||
console.error(error);
|
||||
} finally {
|
||||
console.log("done");
|
||||
}
|
||||
}, [lineName, selectedPeople]);
|
||||
|
||||
const handleCancel = () => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const keyMap: KeyMap = {
|
||||
HANDLE_SEARCH: "enter",
|
||||
};
|
||||
const handlers = {
|
||||
HANDLE_SEARCH: onSearch,
|
||||
};
|
||||
|
||||
if (isLoading) return <span>one second while we make magic</span>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="Create a Line"
|
||||
visible={open}
|
||||
onCancel={handleCancel}
|
||||
footer={
|
||||
<div className="flex flex-row text-white">
|
||||
<button
|
||||
className="flex-1 bg-gray-500 pb-3 pt-2 text-left pl-2"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 bg-teal-500 pb-3 pt-2 text-left pl-2"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
className={"flex flex-col gap-5"}
|
||||
>
|
||||
<HotKeys handlers={handlers} keyMap={keyMap} allowChanges={true}>
|
||||
<div className="flex flex-col items-start gap-2 mb-5">
|
||||
<p className="text-gray-300 text-sm">People</p>
|
||||
|
||||
<span className="flex flex-row gap-1 w-full items-center border border-gray-200 p-2 shadow">
|
||||
<FiSearch className="text-gray-300" />
|
||||
|
||||
<input
|
||||
className="placeholder:text-gray-300 outline-none placeholder:text-sm border-0 flex-1"
|
||||
value={peopleSearchValue}
|
||||
onChange={(e) => setPeopleSearchValue(e.target.value)}
|
||||
placeholder="search by name or email"
|
||||
/>
|
||||
|
||||
<span className="text-xs text-gray-200 ml-auto">
|
||||
enter to search
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* search results */}
|
||||
{searchRes?.users?.length > 0 && (
|
||||
<div className="flex flex-col border border-gray-200 shadow-md max-h-[500px] w-full overflow-y-auto">
|
||||
{searchRes?.users?.map((searchedUser) => (
|
||||
<BasicUserRow
|
||||
key={`searchResUser-${searchedUser.googleId}`}
|
||||
user={searchedUser}
|
||||
rightJsx={
|
||||
<button onClick={() => selectUser(searchedUser)}>
|
||||
Add
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* selected people */}
|
||||
{selectedPeople?.length > 0 && (
|
||||
<div className="flex flex-col gap-2 mb-5">
|
||||
<p className="text-gray-300 text-sm">Selected People</p>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
{selectedPeople.map((selectedUser) => (
|
||||
<BasicUserRow
|
||||
key={`selectedUser-${selectedUser.googleId}`}
|
||||
user={selectedUser}
|
||||
rightJsx={
|
||||
<button
|
||||
onClick={() =>
|
||||
unSelectUser(selectedUser._id.toString())
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-gray-300 text-sm">Line Name (optional)</p>
|
||||
<input
|
||||
value={lineName}
|
||||
onChange={(e) => setLineName(e.target.value)}
|
||||
className="placeholder:text-gray-300 outline-none placeholder:text-sm flex-1
|
||||
border p-2 border-gray-200"
|
||||
placeholder={"ex. Engineering, Sprint 7, Follow up on present..."}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user