throwing away bunch of garbage
This commit is contained in:
+3
-3
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Channels from '../electron/constants';
|
||||
import { FcGoogle } from 'react-icons/fc';
|
||||
// import Logo from '../../components/Logo';
|
||||
import { login } from '../../../api/NirvanaApi';
|
||||
import { login } from '../api/NirvanaApi';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import Channels from '../../../electron/constants';
|
||||
import useAuth from '../../../providers/AuthProvider';
|
||||
import useAuth from '../providers/AuthProvider';
|
||||
|
||||
export default function Login() {
|
||||
const { setJwtToken } = useAuth();
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { RoomsProvider } from '../providers/RoomsProvider';
|
||||
import { TerminalProvider } from '../providers/TerminalProvider';
|
||||
import { AuthProvider } from '../providers/AuthProvider';
|
||||
import { ElectronProvider } from '../providers/ElectronProvider';
|
||||
import ProtectedRoute from './ProtectedRoute';
|
||||
import React from 'react';
|
||||
import { RoomsProvider } from '../providers/RoomsProvider';
|
||||
import { SocketProvider } from '../providers/SocketProvider';
|
||||
import ProtectedRoute from './protected/ProtectedRoute';
|
||||
import { StabilityProvider } from '../providers/StabilityProvider';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
export default function ElectronApp() {
|
||||
return (
|
||||
<StabilityProvider>
|
||||
@@ -15,9 +14,7 @@ export default function ElectronApp() {
|
||||
<ProtectedRoute>
|
||||
<SocketProvider>
|
||||
<RoomsProvider>
|
||||
<TerminalProvider>
|
||||
<></>
|
||||
</TerminalProvider>
|
||||
<></>
|
||||
</RoomsProvider>
|
||||
</SocketProvider>
|
||||
</ProtectedRoute>
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
import { Avatar, Divider, Skeleton, Spin } from 'antd';
|
||||
import { FiPlusSquare, FiUsers, FiX, FiXSquare } from 'react-icons/fi';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createLine, userSearch } from '../../../../api/NirvanaApi';
|
||||
import { useAsyncFn, useDebounce, useKeyPressEvent } from 'react-use';
|
||||
|
||||
import CreateLineRequest from '@nirvana/core/requests/createLine.request';
|
||||
import { User } from '@nirvana/core/models/user.model';
|
||||
import { maxChannelUserCount } from '../rules';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function NewChannelForm({ handleClose }: { handleClose: () => void }) {
|
||||
const [peopleSearchQuery, setPeopleSearchQuery] = useState<string>('');
|
||||
|
||||
const [userSearchRes, fetchUsers] = useAsyncFn(userSearch);
|
||||
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
|
||||
|
||||
const [isSearchingUsers, setSearchingUsers] = useState<boolean>(false);
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [createChannelRes, triggerCreateChannel] = useAsyncFn(createLine);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchInputRef) searchInputRef.current.focus();
|
||||
}, [searchInputRef]);
|
||||
|
||||
const [_, cancel] = useDebounce(
|
||||
async () => {
|
||||
if (!peopleSearchQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchUsers(peopleSearchQuery);
|
||||
|
||||
setSearchingUsers(false);
|
||||
} catch (error) {
|
||||
toast.error('Problem in searching users!');
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
200,
|
||||
[peopleSearchQuery, setSearchingUsers, fetchUsers],
|
||||
);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
async (e) => {
|
||||
setSearchingUsers(true);
|
||||
setPeopleSearchQuery(e.target.value);
|
||||
},
|
||||
[setPeopleSearchQuery, setSearchingUsers],
|
||||
);
|
||||
|
||||
// ensuring that we haven't already selected this user
|
||||
// changing search results so that we don't see the selected user in the search results anymore
|
||||
const addUser = useCallback(
|
||||
(newUser: User) => {
|
||||
setSelectedUsers((prevUsers) => {
|
||||
if (prevUsers.find((currUser) => currUser.email === newUser.email)) {
|
||||
return prevUsers;
|
||||
}
|
||||
|
||||
if (prevUsers.length === maxChannelUserCount - 1) {
|
||||
toast.error('you can only have 8 people per channel!');
|
||||
|
||||
return prevUsers;
|
||||
}
|
||||
|
||||
return [...prevUsers, newUser];
|
||||
});
|
||||
|
||||
if (userSearchRes.value?.users) {
|
||||
userSearchRes.value.users = userSearchRes.value.users.filter(
|
||||
(currUser) => currUser._id !== newUser._id,
|
||||
);
|
||||
}
|
||||
},
|
||||
[setSelectedUsers, userSearchRes.value],
|
||||
);
|
||||
|
||||
const removeUser = useCallback((userIdToRemove: string) => {
|
||||
setSelectedUsers((prevUsers) =>
|
||||
prevUsers.filter((currentUser) => currentUser._id.toString() !== userIdToRemove),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 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
|
||||
const handleCreateChannel = useCallback(async () => {
|
||||
console.log('trying to create line now!');
|
||||
|
||||
try {
|
||||
if (!selectedUsers?.length) {
|
||||
toast.error('you must select at least one person');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedUsers.length === maxChannelUserCount - 1) {
|
||||
toast.error(`Max ${maxChannelUserCount} people per channel including you!`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedMemberIds = selectedUsers.map((selectedPerson) =>
|
||||
selectedPerson._id.toString(),
|
||||
);
|
||||
|
||||
await triggerCreateChannel(new CreateLineRequest(selectedMemberIds));
|
||||
|
||||
toast.success('created channel!');
|
||||
|
||||
// handle close once the new line is created
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
toast.error(error);
|
||||
console.error(error);
|
||||
} finally {
|
||||
console.log('done');
|
||||
}
|
||||
}, [handleClose, selectedUsers, triggerCreateChannel]);
|
||||
|
||||
useKeyPressEvent('Enter', handleCreateChannel);
|
||||
useKeyPressEvent('Escape', handleClose);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center pt-10 bg-white relative">
|
||||
<span className="flex flex-col items-center gap-2 absolute top-5 right-5 cursor-pointer">
|
||||
<FiX onClick={handleClose} className="text-gray-300 text-xl" />
|
||||
<span className="text-gray-300 text-xs p-1 bg-gray-100">`esc`</span>
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-2 max-w-lg w-full">
|
||||
{/* people search */}
|
||||
<span className="text-gray-500">People</span>
|
||||
<div className="flex flex-row items-center space-x-2 bg-gray-100 p-3 rounded">
|
||||
<FiUsers className="text-lg text-gray-400" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
placeholder="Search by name or email"
|
||||
className="flex-1 text-lg bg-transparent placeholder-gray-300 focus:outline-none"
|
||||
onChange={handleSearchChange}
|
||||
value={peopleSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* dropdown search results */}
|
||||
<div className="flex flex-col shadow-lg max-h-[500px] overflow-auto">
|
||||
{(userSearchRes.loading || isSearchingUsers) && <Spin />}
|
||||
|
||||
{(!userSearchRes.value || userSearchRes.value?.users.length === 0) &&
|
||||
selectedUsers?.length === 0 && (
|
||||
<span className="p-10 text-gray-300">{`Can't find someone? Invite them and tell them the secret passcode!`}</span>
|
||||
)}
|
||||
{userSearchRes.value?.users.map((searchedUser) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => addUser(searchedUser)}
|
||||
role={'presentation'}
|
||||
key={searchedUser.email}
|
||||
className="flex flex-row gap-2 items-center p-2 border border-gray-200
|
||||
hover:bg-gray-100 cursor-pointer"
|
||||
>
|
||||
<Avatar src={searchedUser.picture} size={'large'} shape={'square'} />
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="text-md font-semibold text-gray-600">{searchedUser.name}</span>
|
||||
<span className="text-sm text-gray-400">{searchedUser.email}</span>
|
||||
</span>
|
||||
|
||||
<FiPlusSquare className="ml-auto text-lg text-teal-500 cursor-pointer group-hover:scale-105" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* selected people */}
|
||||
<span className="flex flex-row justify-between mt-5">
|
||||
<span className="text-gray-500 ">{`Selected`}</span>
|
||||
|
||||
<span className="text-gray-400">
|
||||
{`${selectedUsers.length}/${maxChannelUserCount - 1}`}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{selectedUsers.map((selectedUser) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => removeUser(selectedUser._id.toString())}
|
||||
role={'presentation'}
|
||||
key={selectedUser.email}
|
||||
className="flex flex-row gap-2 items-center p-2 border border-gray-200
|
||||
hover:bg-gray-100 cursor-pointer group"
|
||||
>
|
||||
<Avatar src={selectedUser.picture} size={'large'} shape={'square'} />
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="text-md font-semibold text-gray-600">{selectedUser.name}</span>
|
||||
<span className="text-sm text-gray-400">{selectedUser.email}</span>
|
||||
</span>
|
||||
|
||||
<FiXSquare className="ml-auto text-lg text-pink-500 cursor-pointer group-hover:scale-105" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="flex flex-row justify-end items-start gap-2">
|
||||
<button onClick={handleClose} className="p-2 text-gray-300 hover:bg-gray-100">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<span className="flex flex-col items-center gap-2">
|
||||
<button onClick={handleCreateChannel} className="p-2 bg-gray-800 text-white">
|
||||
Tune In
|
||||
</button>
|
||||
<span className="text-gray-300 text-xs p-1 bg-gray-100">`enter`</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
import React, { useMemo, useEffect, useRef } from 'react';
|
||||
import useAuth from '../../../../providers/AuthProvider';
|
||||
import useStreams from '../../../../providers/StreamProvider';
|
||||
|
||||
import { LineMemberState } from '@nirvana/core/models/line.model';
|
||||
import LineIcon from '../../../../components/lineIcon';
|
||||
import { FiActivity, FiSettings, FiSun } from 'react-icons/fi';
|
||||
import { Avatar, Spin, Tooltip } from 'antd';
|
||||
import useTerminalProvider from '../../../../providers/TerminalProvider';
|
||||
|
||||
export default function LineDetails() {
|
||||
const { user } = useAuth();
|
||||
|
||||
const { selectedLineId, allChannels, handleUpdateLineMemberState } = useTerminalProvider();
|
||||
|
||||
const selectedLine = useMemo(
|
||||
() =>
|
||||
allChannels.find((currChannel) => currChannel.lineDetails._id.toString() === selectedLineId),
|
||||
[selectedLineId, allChannels],
|
||||
);
|
||||
|
||||
const isUserToggleTuned = useMemo(
|
||||
() => selectedLine?.currentUserMember?.state === LineMemberState.TUNED,
|
||||
[selectedLine],
|
||||
);
|
||||
|
||||
const { peerMap } = useStreams();
|
||||
|
||||
const isUserBroadcasting = useMemo(
|
||||
() => selectedLine?.currentBroadcastersUserIds?.includes(user._id.toString()),
|
||||
[user, selectedLine],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 bg-white relative overflow-auto">
|
||||
{/* line details */}
|
||||
<div
|
||||
className="p-5 z-30 titlebar
|
||||
flex flex-row items-center justify-end border-b-gray-200 border-b shadow-2xl group"
|
||||
>
|
||||
{/* channel picture */}
|
||||
{selectedLine.profilePictures && (
|
||||
<LineIcon
|
||||
grayscale={!selectedLine.isUserTunedIn}
|
||||
sourceImages={
|
||||
selectedLine.profilePictures.tunedMembers.length > 0
|
||||
? selectedLine.profilePictures.tunedMembers
|
||||
: selectedLine.profilePictures.allMembersWithoutMe
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="ml-2 mr-auto flex flex-col items-start ">
|
||||
<span className="flex flex-row gap-2 items-center">
|
||||
<h2 className={`text-md text-gray-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>
|
||||
</div>
|
||||
|
||||
<Avatar.Group className={'animate-pulse'}>
|
||||
<Avatar
|
||||
key={`lineTunedInUserAvatar-${-1}`}
|
||||
src={user.picture}
|
||||
shape="square"
|
||||
size={'large'}
|
||||
className={`shadow-lg`}
|
||||
/>
|
||||
|
||||
{selectedLine.profilePictures?.tunedMembers?.map((pictureSrc, index) => (
|
||||
<Avatar
|
||||
key={`lineTunedInUserAvatar-${index}`}
|
||||
src={pictureSrc}
|
||||
shape="square"
|
||||
size={'large'}
|
||||
className={`shadow-lg`}
|
||||
/>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
|
||||
<span className="px-10 text-gray-200"> | </span>
|
||||
|
||||
<Avatar.Group>
|
||||
{selectedLine.profilePictures.untunedMembers.map((pictureSrc, index) => (
|
||||
<Avatar
|
||||
key={`lineOfflineUserAvatar-${index}`}
|
||||
src={pictureSrc}
|
||||
shape="square"
|
||||
size={'default'}
|
||||
// grayscale if not playing?
|
||||
className={`grayscale`}
|
||||
/>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
</div>
|
||||
|
||||
{/* main canvas */}
|
||||
<div className="flex flex-col flex-1">
|
||||
{/* line timeline */}
|
||||
{/* <LineHistory /> */}
|
||||
|
||||
{/* live line */}
|
||||
<div
|
||||
className="flex-1 flex flex-col justify-start items-center
|
||||
gap-2 p-5 mx-auto max-w-lg w-full"
|
||||
>
|
||||
<span className="flex flex-row gap-2 items-center mx-auto text-center mt-5 text-teal-500">
|
||||
<FiSun />
|
||||
<span>Right now</span>
|
||||
</span>
|
||||
|
||||
{Object.keys(peerMap).map((lineId, index) => {
|
||||
if (lineId !== selectedLine.lineDetails._id.toString()) return <></>;
|
||||
|
||||
return peerMap[lineId]?.peerRelations?.map(
|
||||
(linePeer) =>
|
||||
linePeer?.peerMediaStream && (
|
||||
<StreamPlayer
|
||||
key={`streamPlayer-${index}`}
|
||||
peerStream={linePeer.peerMediaStream}
|
||||
/>
|
||||
),
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* canvas action buttons */}
|
||||
<div className="absolute right-5 bottom-5 flex flex-row gap-3 p-10 justify-end items-center ">
|
||||
<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
|
||||
? handleUpdateLineMemberState(
|
||||
selectedLine.lineDetails._id.toString(),
|
||||
LineMemberState.INBOX,
|
||||
)
|
||||
: handleUpdateLineMemberState(
|
||||
selectedLine.lineDetails._id.toString(),
|
||||
LineMemberState.TUNED,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FiActivity className="text-md" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={'Press and hold ` or click to join the room'}>
|
||||
<button
|
||||
className={`p-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>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamPlayer({ peerStream }: { peerStream: MediaStream }) {
|
||||
const streamRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (streamRef?.current) streamRef.current.srcObject = peerStream;
|
||||
}, [peerStream]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <video
|
||||
ref={streamRef}
|
||||
height={300}
|
||||
width={400}
|
||||
className={'shadow-xl rounded'}
|
||||
autoPlay
|
||||
muted
|
||||
/> */}
|
||||
<audio ref={streamRef} className={'shadow-xl rounded'} autoPlay controls />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// function LineHistory() {
|
||||
// return (
|
||||
// <div
|
||||
// className="flex-1 flex flex-col justify-start items-center gap-2 p-5 mx-auto
|
||||
// max-w-lg w-full bg-white"
|
||||
// >
|
||||
// <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 flex flex-row items-center gap-2 w-full
|
||||
// p-5 border-gray-200 border`}
|
||||
// >
|
||||
// <Avatar.Group key={`lineHistoryMessage-yesterday-afternoon}`}>
|
||||
// {selectedLine.otherUserObjects.map((otherUser) => (
|
||||
// <Avatar
|
||||
// key={`linehistory-${1}`}
|
||||
// src={otherUser.picture}
|
||||
// shape="square"
|
||||
// size={'default'}
|
||||
// // grayscale if not playing?
|
||||
// className={`${true && 'grayscale'}`}
|
||||
// />
|
||||
// ))}
|
||||
// </Avatar.Group>
|
||||
|
||||
// {selectedLine.otherUserObjects.map((otherUser) => (
|
||||
// <span key={`chunk-${otherUser.name}`} 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>
|
||||
|
||||
// <span className={'text-gray-300 text-sm'}>today</span>
|
||||
|
||||
// <div
|
||||
// className={`rounded flex flex-row items-center gap-2 w-full shadow-lg p-5
|
||||
// border-gray-200 border`}
|
||||
// >
|
||||
// <Avatar.Group key={`lineHistoryMessage-yesterday-afternoon}`}>
|
||||
// {selectedLine.otherUserObjects.map((otherUser) => (
|
||||
// <Avatar
|
||||
// key={`linehistory-${1}`}
|
||||
// src={otherUser.picture}
|
||||
// shape="square"
|
||||
// size={'default'}
|
||||
// // grayscale if not playing?
|
||||
// className={`${true && 'grayscale'}`}
|
||||
// />
|
||||
// ))}
|
||||
// </Avatar.Group>
|
||||
|
||||
// {selectedLine.otherUserObjects.map((otherUser) => (
|
||||
// <span key={`chunk-${otherUser.name}`} 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>
|
||||
|
||||
// <span className={'text-teal-500 text-sm flex flex-row gap-2 items-center mt-5'}>
|
||||
// <FiSun />
|
||||
// <span>right now</span>
|
||||
// </span>
|
||||
|
||||
// {/* live broadcasters */}
|
||||
// <div className="flex flex-col w-full gap-2 shadow-2xl border border-teal-500 rounded">
|
||||
// {selectedLine.otherUserObjects.map((otherUser) => (
|
||||
// <div key={otherUser.email} className="flex flex-row items-center gap-2 p-4">
|
||||
// <Avatar
|
||||
// key={`linehistory-${1}`}
|
||||
// src={otherUser.picture}
|
||||
// shape="square"
|
||||
// size={'large'}
|
||||
// />
|
||||
|
||||
// <span className="text-gray-600 font-semibold">{otherUser.name}</span>
|
||||
|
||||
// <FiHeadphones className="ml-auto text-lg" />
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
@@ -1,142 +0,0 @@
|
||||
import { Avatar, Tooltip } from 'antd';
|
||||
import { FiSun, FiX } from 'react-icons/fi';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import LineIcon from '../../../../components/lineIcon';
|
||||
import MasterLineData from '@nirvana/core/models/masterLineData.model';
|
||||
import { maxToggleTunedChannelCount } from '../rules';
|
||||
import moment from 'moment';
|
||||
import useAuth from '../../../../providers/AuthProvider';
|
||||
import useElectron from '../../../../providers/ElectronProvider';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
|
||||
export default React.memo(function LineRow({
|
||||
index,
|
||||
line,
|
||||
handleSelectLine,
|
||||
isSelected,
|
||||
}: {
|
||||
index: number; // the order of this one in the list (for this view to know the shortcut to register)
|
||||
line: MasterLineData;
|
||||
handleSelectLine: (newLineId: string) => void;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
const { user } = useAuth();
|
||||
const { desktopMode, isWindowFocused } = useElectron();
|
||||
|
||||
const handleActivateLine = useCallback(() => {
|
||||
handleSelectLine(line.lineDetails._id.toString());
|
||||
}, [handleSelectLine, line.lineDetails, index]);
|
||||
|
||||
const hotkeyActivateLine = useCallback(() => {
|
||||
// TODO: disable for higher numbers? ehh maybe hidden easter egg to select more?
|
||||
|
||||
// if (isUserToggleTuned) handleActivateLine();
|
||||
|
||||
handleActivateLine();
|
||||
}, [handleActivateLine]);
|
||||
|
||||
useKeyPressEvent((index + 1).toString(), hotkeyActivateLine);
|
||||
|
||||
const renderRightActivity = useMemo(() => {
|
||||
if (isSelected) {
|
||||
return (
|
||||
<Tooltip title={'esc'}>
|
||||
<span className="flex flex-col items-center gap-2 cursor-pointer">
|
||||
<FiX className="text-gray-400 text-xl" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (line.profilePictures.broadcastMembers.length > 0)
|
||||
return (
|
||||
<Avatar.Group
|
||||
maxCount={2}
|
||||
maxPopoverTrigger="click"
|
||||
size="small"
|
||||
maxStyle={{
|
||||
color: '#f56a00',
|
||||
backgroundColor: '#fde3cf',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '0',
|
||||
}}
|
||||
className="shadow-lg"
|
||||
>
|
||||
{line.profilePictures.broadcastMembers.map((pictureSrc, index) => (
|
||||
<Avatar
|
||||
key={`lineRowActiveBroadcasters-${index}`}
|
||||
src={pictureSrc}
|
||||
shape="square"
|
||||
size={'small'}
|
||||
/>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
);
|
||||
|
||||
if (line.profilePictures.tunedMembers.length > 0)
|
||||
return <FiSun className="text-teal-500 animate-pulse" />;
|
||||
|
||||
// if there is new activity blocks for me
|
||||
if (line.currentUserMember.lastVisitDate)
|
||||
return <span className="h-2 w-2 rounded-full bg-slate-800 animate-pulse"></span>;
|
||||
|
||||
// TODO: compare last visit date to latest content block
|
||||
if (line.currentUserMember)
|
||||
return (
|
||||
<span className={`text-gray-400 ml-auto text-xs font-semibold`}>
|
||||
{moment(line.currentUserMember.lastVisitDate).fromNow(true)}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={`text-gray-200 ml-auto text-xs `}>
|
||||
{moment(line.currentUserMember.lastVisitDate).fromNow(true)}
|
||||
</span>
|
||||
);
|
||||
}, [line, isSelected]);
|
||||
|
||||
// TODO: low priority: scale the whole thing and make it pop out nad translate...
|
||||
// doesn't work right now because no workaround for overflow scroll for y and visible for x
|
||||
return (
|
||||
<div
|
||||
onClick={handleActivateLine}
|
||||
role={'presentation'}
|
||||
className={`flex flex-row items-center justify-start gap-2 px-4 py-4 hover:bg-gray-200
|
||||
cursor-pointer transition-all relative z-50
|
||||
|
||||
${line.isUserToggleTuned && ' bg-gray-100 shadow-2xl'}
|
||||
|
||||
${line.isUserTunedIn && isSelected && ' bg-gray-200 shadow-2xl'}`}
|
||||
>
|
||||
{/* channel picture */}
|
||||
{line.profilePictures && (
|
||||
<span className={`${isSelected && ' scale-125 transition-all'}`}>
|
||||
<LineIcon
|
||||
grayscale={!line.isUserTunedIn}
|
||||
sourceImages={
|
||||
line.profilePictures.tunedMembers.length > 0
|
||||
? line.profilePictures.tunedMembers
|
||||
: line.profilePictures.allMembersWithoutMe
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* channel name */}
|
||||
<span
|
||||
className={`text-md max-w-[180px] truncate ${
|
||||
line.currentUserMember.lastVisitDate ? 'font-semibold text-gray-800' : ' text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{line.lineDetails.name || line.otherUserObjects[0].givenName}
|
||||
</span>
|
||||
|
||||
{index < maxToggleTunedChannelCount && (
|
||||
<span className="ml-2 text-gray-300 text-xs p-1 px-2 bg-gray-100">{`${index + 1}`}</span>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex flex-shrink-0">{renderRightActivity}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Avatar, Dropdown, Menu } from 'antd';
|
||||
import { FiLogOut, FiSearch } from 'react-icons/fi';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { FaPlus, FaSearch } from 'react-icons/fa';
|
||||
import useAuth from '../../../../providers/AuthProvider';
|
||||
import useElectron from '../../../../providers/ElectronProvider';
|
||||
import useSockets from '../../../../providers/SocketProvider';
|
||||
import NoTextLogo from '@nirvana/components/logo/NoTextLogo';
|
||||
|
||||
/**
|
||||
* TODO: add in video mode
|
||||
*
|
||||
*/
|
||||
export default function NavBar() {
|
||||
const { user, handleLogout } = useAuth();
|
||||
const { desktopMode, handleToggleDesktopMode } = useElectron();
|
||||
|
||||
const { handleFlowState } = useSockets();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
/** hide the search bar in the header so that it's cleaner for these two modes */
|
||||
const shouldHideSearch = useMemo(() => {
|
||||
if (desktopMode === 'overlayOnly') return true;
|
||||
|
||||
return false;
|
||||
}, [desktopMode]);
|
||||
|
||||
const selectSearch = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
// 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: (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleLogout();
|
||||
}}
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
),
|
||||
icon: <FiLogOut />,
|
||||
key: `profile-menu-${5}`,
|
||||
danger: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-3 items-center bg-gray-100 p-4 pb-0" id="titlebar">
|
||||
<Dropdown overlay={profileMenu}>
|
||||
<div className={'cursor-pointer'}>
|
||||
{user.picture && (
|
||||
<Avatar
|
||||
key={`userHeaderProfilePicture`}
|
||||
className="shadow-md hover:scale-110 transition-all"
|
||||
size={'default'}
|
||||
alt={user.name}
|
||||
src={user.picture}
|
||||
shape="square"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
<span className="font-semibold mx-auto">Channels</span>
|
||||
|
||||
<button
|
||||
onClick={handleFlowState}
|
||||
className="text-gray-300 text-xs p-3 transition-all hover:bg-gray-200"
|
||||
>
|
||||
flow
|
||||
</button>
|
||||
|
||||
{/* 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,32 +0,0 @@
|
||||
import useAuth from '../../../../providers/AuthProvider';
|
||||
|
||||
import NewChannelForm from '../compose/NewChannelForm';
|
||||
import LineDetails from '../line/LineDetails';
|
||||
import useTerminalProvider from '../../../../providers/TerminalProvider';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default function MainPanel() {
|
||||
const { user } = useAuth();
|
||||
|
||||
const { selectedLineId, showNewChannelForm, handleShowNewChannelForm } = useTerminalProvider();
|
||||
|
||||
// already won't see stuff for overlay only mode as per parent configuration
|
||||
|
||||
// TODO: if there is stuff in search, show that first
|
||||
|
||||
if (showNewChannelForm)
|
||||
return <NewChannelForm handleClose={() => handleShowNewChannelForm('hide')} />;
|
||||
|
||||
// if selected line, show line details
|
||||
if (selectedLineId) return <LineDetails />;
|
||||
|
||||
// else show the stale state
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 justify-center items-center bg-white">
|
||||
<span className="text-xl text-gray-800">{`Hi ${user.givenName}!`}</span>
|
||||
<span className="text-md text-gray-400">{"You're all set!"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
import { Avatar, Dropdown, Skeleton, Tooltip } from 'antd';
|
||||
import { FiActivity, FiPlus, FiSearch } from 'react-icons/fi';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { LineMemberState } from '@nirvana/core/models/line.model';
|
||||
import LineRow from '../line/LineRow';
|
||||
import NavBar from '../navbar/Navbar';
|
||||
import NoTextLogo from '@nirvana/components/logo/NoTextLogo';
|
||||
import { maxToggleTunedChannelCount } from '../rules';
|
||||
import useAuth from '../../../../providers/AuthProvider';
|
||||
import useElectron from '../../../../providers/ElectronProvider';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import useRooms from '../../../../providers/RoomsProvider';
|
||||
import useSockets from '../../../../providers/SocketProvider';
|
||||
import useStreams from '../../../../providers/StreamProvider';
|
||||
import useTerminalProvider from '../../../../providers/TerminalProvider';
|
||||
|
||||
export default function SidePanel() {
|
||||
// using merely for loading state...better to add to realtimeroom context?
|
||||
const { rooms: initialRoomsFetch } = useRooms();
|
||||
|
||||
const { user, handleLogout } = useAuth();
|
||||
|
||||
const {
|
||||
allChannels,
|
||||
tunedChannelsCount,
|
||||
handleSelectLine,
|
||||
selectedLineId,
|
||||
handleShowNewChannelForm,
|
||||
} = useTerminalProvider();
|
||||
|
||||
const { handleFlowState } = useSockets();
|
||||
|
||||
const { handleToggleDesktopMode, desktopMode, isWindowFocused } = useElectron();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
const handleCreateNewChannel = useCallback(() => {
|
||||
handleShowNewChannelForm('show');
|
||||
}, [handleShowNewChannelForm]);
|
||||
|
||||
const omniSearchBarRef = useRef<HTMLInputElement>();
|
||||
|
||||
const focusSearch = useCallback(() => {
|
||||
if (omniSearchBarRef.current) omniSearchBarRef.current.focus();
|
||||
}, [omniSearchBarRef]);
|
||||
|
||||
useKeyPressEvent('Tab', focusSearch);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col w-[350px] group
|
||||
border-r border-r-gray-200 shadow-xl z-20 bg-white
|
||||
|
||||
${!isWindowFocused && desktopMode === 'overlayOnly' && ' opacity-20 '}`}
|
||||
>
|
||||
{/* user control panel */}
|
||||
<div
|
||||
className={`bg-gray-100 flex flex-row items-center gap-2
|
||||
p-4 pb-2 z-50 w-full titlebar ${
|
||||
desktopMode === 'overlayOnly' && 'border-b border-b-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<button onClick={handleToggleDesktopMode} className={'mr-auto animate-pulse'}>
|
||||
<NoTextLogo type="small" />
|
||||
</button>
|
||||
|
||||
{desktopMode === 'mainApp' && (
|
||||
<span className="text-gray-800 font-semibold mx-auto">Conversations</span>
|
||||
)}
|
||||
|
||||
{/* <button
|
||||
onClick={handleLogout}
|
||||
className="text-gray-300 text-xs px-3 py-2 transition-all hover:bg-gray-200"
|
||||
>
|
||||
log out
|
||||
</button> */}
|
||||
|
||||
<button
|
||||
onClick={handleFlowState}
|
||||
className="text-gray-300 text-xs px-3 py-2 transition-all hover:bg-gray-200"
|
||||
>
|
||||
flow
|
||||
</button>
|
||||
|
||||
<UserProfileAvatar />
|
||||
</div>
|
||||
|
||||
{/* search + other stuff */}
|
||||
{desktopMode === 'mainApp' && (
|
||||
<>
|
||||
<div className="flex flex-row p-4 items-center bg-gray-100 gap-2">
|
||||
<div className="flex-1 flex flex-row items-center space-x-2 bg-gray-200 p-2 rounded">
|
||||
<FiSearch className="text-xs text-gray-400" />
|
||||
<input
|
||||
placeholder="Find or start a conversation"
|
||||
className="flex-1 bg-transparent placeholder-gray-400 text-gray-500 placeholder:text-xs focus:outline-none"
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery}
|
||||
ref={omniSearchBarRef}
|
||||
/>
|
||||
|
||||
<span className="ml-auto text-gray-300 text-xs p-1 bg-gray-100">tab</span>
|
||||
</div>
|
||||
|
||||
<Tooltip title={'New channel'}>
|
||||
<button
|
||||
onClick={handleCreateNewChannel}
|
||||
className="ml-auto flex flex-row items-center justify-evenly
|
||||
shadow-xl bg-gray-800 p-2 text-white text-xs"
|
||||
>
|
||||
<FiPlus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col shadow-xl bg-gray-100 pb-2">
|
||||
<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-xs">Priority</h2>
|
||||
|
||||
<p className="text-slate-300 text-xs ml-auto">{`${
|
||||
tunedChannelsCount || 0
|
||||
}/${maxToggleTunedChannelCount}`}</p>
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!(allChannels.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-1 overflow-y-auto flipped'}>
|
||||
<div className="flex flex-col direction-ltr">
|
||||
{initialRoomsFetch.loading ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
allChannels.map((masterLineData, index) => (
|
||||
<LineRow
|
||||
index={index}
|
||||
key={`terminalListLines-${masterLineData.lineDetails._id.toString()}`}
|
||||
line={masterLineData}
|
||||
handleSelectLine={handleSelectLine}
|
||||
isSelected={masterLineData.lineDetails._id.toString() === selectedLineId}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// todo if audio only mode, show avatar
|
||||
// if problem, show error
|
||||
function UserProfileAvatar() {
|
||||
const { user } = useAuth();
|
||||
const { userLocalStream } = useStreams();
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current && userLocalStream) videoRef.current.srcObject = userLocalStream;
|
||||
}, [userLocalStream]);
|
||||
|
||||
if (userLocalStream) return <video ref={videoRef} muted height={'50'} width={'50'} autoPlay />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{user.picture && (
|
||||
<Avatar
|
||||
key={`userHeaderProfilePicture`}
|
||||
className="shadow-md hover:scale-110 transition-all"
|
||||
size={'default'}
|
||||
alt={user.name}
|
||||
src={user.picture}
|
||||
shape="square"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const maxChannelUserCount = 8;
|
||||
|
||||
export const maxToggleTunedChannelCount = 3;
|
||||
Reference in New Issue
Block a user