throwing away bunch of garbage

This commit is contained in:
talksik
2022-06-10 05:29:30 -05:00
parent 7af1c1ceb0
commit 445d1ef522
32 changed files with 120 additions and 2471 deletions
-11
View File
@@ -2,12 +2,9 @@ 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 '@nirvana/core/responses/login.response';
import MasterLineData from '@nirvana/core/models/masterLineData.model';
import NirvanaResponse from '@nirvana/core/responses/nirvanaResponse';
import UpdateLineMemberState from '@nirvana/core/requests/updateLineMemberState.request';
import { User } from '@nirvana/core/models';
import UserDetailsResponse from '@nirvana/core/responses/userDetails.response';
import UserSearchResponse from '@nirvana/core/responses/userSearch.response';
@@ -94,11 +91,3 @@ export async function updateLineMemberState(
request,
);
}
export async function getDmByUserId(otherUserId: string): Promise<Line> {
return await NirvanaApi.fetch(`/lines/dm/${otherUserId}`, 'GET', true);
}
export async function createLine(request: CreateLineRequest): Promise<NirvanaResponse<Line>> {
return await NirvanaApi.fetch(`/lines`, 'POST', true, request);
}
@@ -2,7 +2,7 @@ import NirvanaApi, { getUserDetails } from '../api/NirvanaApi';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { STORE_ITEMS } from '../electron/constants';
import { User } from '@nirvana/core/models/user.model';
import User from '@nirvana/core/models/user.model';
import toast from 'react-hot-toast';
import { useAsyncFn } from 'react-use';
@@ -1,7 +1,7 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { Socket, io } from 'socket.io-client';
import FlowState from '../tree/protected/FlowState';
import FlowState from '../tree/FlowState';
import toast from 'react-hot-toast';
import useAuth from './AuthProvider';
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useContext } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useAsyncFn, useAsyncRetry } from 'react-use';
import { serverCheck } from '../api/NirvanaApi';
interface IStabilityContext {
@@ -1,368 +0,0 @@
import React, { useEffect, useContext, useState, useRef, useMemo, useCallback } from 'react';
import Peer from 'simple-peer';
import useAuth from './AuthProvider';
import { useImmer } from 'use-immer';
import useSockets from './SocketProvider';
import {
RtcAnswerSomeoneRequest,
RtcCallRequest,
RtcNewUserJoinedResponse,
RtcReceiveAnswerResponse,
ServerRequestChannels,
ServerResponseChannels,
} from '@nirvana/core/sockets/channels';
import toast from 'react-hot-toast';
import useTerminalProvider from './TerminalProvider';
import MasterLineData from '@nirvana/core/models/masterLineData.model';
import { useEffectOnce } from 'react-use';
const videoConstraints = false;
// {
// frameRate: 30,
// width: { max: 100 },
// height: { max: 200 },
// };
const iceServers = [
// { urls: 'stun:stun.l.google.com:19302' },
// { urls: 'stun:stun.l.google.com:19302' },
// { urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' },
// { urls: 'stun:stun3.l.google.com:19302' },
// { urls: 'stun:stun4.l.google.com:19302' },
// { urls: 'stun:global.stun.twilio.com:3478?transport=udp' },
// {
// url: 'turn:numb.viagenie.ca',
// credential: 'muazkh',
// username: '[email protected]',
// },
// {
// url: 'turn:192.158.29.39:3478?transport=udp',
// credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
// username: '28224511:1379330808',
// },
// {
// url: 'turn:turn.bistri.com:80',
// credential: 'homeo',
// username: 'homeo',
// },
// {
// url: 'turn:turn.anyfirewall.com:443?transport=tcp',
// credential: 'webrtc',
// username: 'webrtc',
// },
{
url: 'turn:openrelay.metered.ca:80',
credential: 'openrelayproject',
username: 'openrelayproject',
},
];
type LineStreamData = {
localStreamForLine?: MediaStream;
peerRelations: {
userId: string;
peer: Peer;
peerMediaStream?: MediaStream;
}[];
};
type LinePeerMap = {
[lineId: string]: LineStreamData;
};
interface IStreamProvider {
peerMap: LinePeerMap;
userLocalStream?: MediaStream;
}
const StreamProviderContext = React.createContext<IStreamProvider>({
peerMap: {},
});
export function StreamProvider({ children }: { children: React.ReactChild }) {
const { roomsMap } = useTerminalProvider();
const { user } = useAuth();
const { $ws } = useSockets();
const [peerMap, updatePeerMap] = useImmer<LinePeerMap>({});
const [userLocalStream, setUserLocalStream] = useState<MediaStream>();
const handleGotPeerRemoteStream = useCallback(
(lineId: string, userId: string, remoteStream: MediaStream) => {
updatePeerMap((draft) => {
// for trickling, if we already have a peer for this line and user, then just replace
const existingUserLinePeerRelation = draft[lineId]?.peerRelations?.find(
(currPeerRelation) => currPeerRelation.userId === userId,
);
if (existingUserLinePeerRelation) {
existingUserLinePeerRelation.peerMediaStream = remoteStream;
}
});
},
[updatePeerMap],
);
const setLocalStreamForLine = useCallback(
(lineId: string, localStreamForLine: MediaStream) => {
updatePeerMap((draft) => {
draft[lineId] = { ...draft[lineId], localStreamForLine };
});
},
[updatePeerMap],
);
const handleAddPeer = useCallback(
(lineId: string, userId: string, peerObj: Peer) => {
updatePeerMap((draft) => {
// for trickling, if we already have a peer for this line and user, then just replace
const existingUserLinePeerRelation = draft[lineId]?.peerRelations?.find(
(currPeerRelation) => currPeerRelation.userId === userId,
);
if (existingUserLinePeerRelation) {
return draft;
}
if (draft[lineId]?.peerRelations) {
draft[lineId].peerRelations.push({ userId, peer: peerObj });
} else {
draft[lineId] = { ...draft[lineId], peerRelations: [{ userId, peer: peerObj }] };
}
});
},
[updatePeerMap],
);
useEffect(() => {
$ws.on(ServerResponseChannels.RTC_NEW_USER_JOINED, (res: RtcNewUserJoinedResponse) => {
toast.success('NEWBIE JOINED!!!');
console.log('someone calling me', res);
const peerForMeAndNewbie = new Peer({
initiator: false,
trickle: true, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times
stream: peerMap[res.lineId]?.localStreamForLine,
config: {
iceServers,
},
});
peerForMeAndNewbie.signal(res.simplePeerSignal);
handleAddPeer(res.lineId, res.userWhoCalled, peerForMeAndNewbie);
// make sure this peer gets destroyed to remove this listener
peerForMeAndNewbie.on('signal', (signal) => {
console.log('sending an answer to the slave', res);
$ws.emit(
ServerRequestChannels.RTC_ANSWER_SOMEONE_FOR_LINE,
new RtcAnswerSomeoneRequest(res.userWhoCalled, res.lineId, signal),
);
});
peerForMeAndNewbie.on('stream', (remoteStream: MediaStream) => {
handleGotPeerRemoteStream(res.lineId, res.userWhoCalled, remoteStream);
});
});
$ws.on(ServerResponseChannels.RTC_RECEIVING_MASTER_ANSWER, (res: RtcReceiveAnswerResponse) => {
toast.success('MASTER gave me an answer!!!');
console.log('master gave me this answer: ', res);
// find this person in peer map
updatePeerMap((draft) => {
const localPeerForMasterAndMe = draft[res.lineId]?.peerRelations?.find(
(currPeerRelationship) => currPeerRelationship.userId === res.masterUserId,
);
if (localPeerForMasterAndMe) localPeerForMasterAndMe.peer.signal(res.simplePeerSignal);
});
});
return () => {
$ws.removeAllListeners(ServerResponseChannels.RTC_NEW_USER_JOINED);
$ws.removeAllListeners(ServerResponseChannels.RTC_RECEIVING_MASTER_ANSWER);
};
}, [updatePeerMap, $ws, userLocalStream, peerMap, handleGotPeerRemoteStream, handleAddPeer]);
useEffect(() => {
navigator.mediaDevices.enumerateDevices().then((devices) => {
const uniqueDevices = [];
const uniqueGroupIds = [];
devices.forEach((device) => {
if (!uniqueGroupIds.includes(device.groupId)) {
uniqueDevices.push(device);
uniqueGroupIds.push(device.groupId);
}
});
console.log(uniqueDevices);
});
// navigator.mediaDevices
// .getUserMedia({
// video: videoConstraints,
// audio: true,
// })
// .then((localMediaStream: MediaStream) => {
// setUserLocalStream(localMediaStream);
// });
}, []);
console.log(`peer map: `, peerMap);
// manage untuning including myself
useEffect(() => {
const tunedUsersForLines: { [lineId: string]: string[] } = {};
Object.values(roomsMap).map((currentLine) => {
if (currentLine.tunedInMemberIds)
tunedUsersForLines[currentLine.lineDetails._id.toString()] = currentLine.tunedInMemberIds;
});
updatePeerMap((draft) => {
// go through peer map
// if there is someone in it who is not in a tuned in line, then destroy peer and remove
Object.entries(draft).map(([lineId, lineStreamData]) => {
// if I left this channel, then I want to make sure to destroy and delete all relations
if (
roomsMap[lineId]?.tunedInMemberIds &&
!roomsMap[lineId]?.tunedInMemberIds.includes(user._id.toString())
) {
lineStreamData?.peerRelations?.forEach((peerRelation) => {
peerRelation.peer.destroy();
});
delete draft[lineId];
return;
}
const usersToRemove = [];
lineStreamData?.peerRelations?.forEach((peerRelation) => {
if (!tunedUsersForLines[lineId].includes(peerRelation.userId)) {
peerRelation.peer.destroy();
usersToRemove.push(peerRelation.userId);
}
});
draft[lineId].peerRelations = draft[lineId]?.peerRelations?.filter(
(peerRelation) => !usersToRemove.includes(peerRelation.userId),
);
});
});
}, [roomsMap, updatePeerMap, user]);
return (
<StreamProviderContext.Provider value={{ peerMap, userLocalStream }}>
{/* handles stream connections */}
{/* {Object.values(roomsMap).map((line) => {
if (line.tunedInMemberIds?.includes(user._id.toString()))
return (
<MemoLineConnector
key={`streamConnector-${line.lineDetails._id.toString()}`}
lineId={line.lineDetails._id.toString()}
handleAddPeer={handleAddPeer}
membersToCall={line.tunedInMemberIds.filter(
(currMemberId) => currMemberId !== user._id.toString(),
)}
handleGotPeerRemoteStream={handleGotPeerRemoteStream}
setLocalStreamForLine={setLocalStreamForLine}
/>
);
})} */}
{children}
</StreamProviderContext.Provider>
);
}
export default function useStreams() {
return useContext(StreamProviderContext);
}
const MemoLineConnector = React.memo(LineConnector);
// handle managing stream connections for one line
function LineConnector({
lineId,
membersToCall,
handleAddPeer,
handleGotPeerRemoteStream,
setLocalStreamForLine,
}: {
lineId: string;
membersToCall: string[];
handleAddPeer: (lineId: string, userId: string, peerObj: Peer) => void;
setLocalStreamForLine: (lineId: string, localStreamForLine: MediaStream) => void;
handleGotPeerRemoteStream: (lineId: string, userId: string, remoteStream: MediaStream) => void;
}) {
const { $ws } = useSockets();
console.log('rendering this piece of shit');
useEffectOnce(() => {
console.log('got initial list for this channel that I am tuned into');
console.log(membersToCall);
// todo get the user media selections
// todo check if already in peer map? keeping it simple for now
// a peer relationship between me and someone for this particular channel so that I can just enable or disable this particular stream
// object instead of managing different ones
// bandwidth wise, would be uploading stream to one room at a time but downloading a x b streams but someone can't
// stream in two at same time anyway
navigator.mediaDevices
.getUserMedia({ video: videoConstraints, audio: true })
.then((localMediaStream: MediaStream) => {
setLocalStreamForLine(lineId, localMediaStream);
toast.success('CALLING bunch of people!!!');
console.log('Calling these folks', membersToCall);
console.log('for line', lineId);
// todo...call in parallel
membersToCall.map((memberId) => {
const connectingToast = toast.loading('calling peer for a snappy experience');
// make sure this peer gets destroyed when it's time to remove this listener
const localPeerConnection = new Peer({
initiator: true,
stream: localMediaStream,
trickle: true, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times,
config: {
iceServers,
},
});
localPeerConnection.on('signal', (signal) => {
console.log('have a signal to make call to someone ');
$ws.emit(
ServerRequestChannels.RTC_CALL_SOMEONE_FOR_LINE,
new RtcCallRequest(memberId, lineId, signal),
);
toast.dismiss(connectingToast);
// sending back the connection to the parent
// so that we can accept the answer later on
handleAddPeer(lineId, memberId, localPeerConnection);
});
localPeerConnection.on('stream', (remoteStream: MediaStream) => {
handleGotPeerRemoteStream(lineId, memberId, remoteStream);
});
});
});
});
return <></>;
}
@@ -1,494 +0,0 @@
import React, { useEffect, useState, useContext, useCallback, useMemo } from 'react';
import useRooms from './RoomsProvider';
import useSockets from './SocketProvider';
import MasterLineData from '@nirvana/core/models/masterLineData.model';
import {
ServerRequestChannels,
ServerResponseChannels,
SomeoneConnectedResponse,
SomeoneDisconnectedResponse,
SomeoneTunedResponse,
SomeoneUntunedFromLineResponse,
TuneToLineRequest,
UserStartedBroadcastingResponse,
UserStoppedBroadcastingResponse,
ConnectToLineRequest,
UntuneFromLineRequest,
} from '@nirvana/core/sockets/channels';
import toast from 'react-hot-toast';
import { useImmer } from 'use-immer';
import { LineMemberState } from '@nirvana/core/models/line.model';
import { useAsyncFn, useKeyPressEvent } from 'react-use';
import { updateLineMemberState } from '../api/NirvanaApi';
import UpdateLineMemberState from '@nirvana/core/requests/updateLineMemberState.request';
import useAuth from './AuthProvider';
import { User } from '@nirvana/core/models/user.model';
import SidePanel from '../tree/protected/terminal/panels/SidePanel';
import MainPanel from '../tree/protected/terminal/panels/MainPanel';
import useElectron from './ElectronProvider';
import { StreamProvider } from './StreamProvider';
type LineIdToMasterLine = {
[lineId: string]: MasterLineData;
};
// TODO: implement the below to save renders
type TunedMembersMap = {
[lineId: string]: string[];
};
type ConnectedMembesMap = {
[lineId: string]: string[];
};
type UserMap = {
[userId: string]: User;
};
type BroadcastersMap = {
[lineId: string]: string[];
};
interface ITerminalProvider {
roomsMap: LineIdToMasterLine;
allChannels: MasterLineData[];
selectedLineId?: string;
handleSelectLine?: (newLineId: string) => void;
handleUpdateLineMemberState?: (lineId: string, newState: LineMemberState) => void;
showNewChannelForm: boolean;
handleShowNewChannelForm?: (showOrHide: 'show' | 'hide') => void;
tunedChannelsCount: number;
}
const TerminalContext = React.createContext<ITerminalProvider>({
roomsMap: {},
allChannels: [],
tunedChannelsCount: 0,
showNewChannelForm: false,
});
/**
*
* handles reads of new data
* keeps listening to incoming socket events to make sure that the realtime rooms map is highly available
*
* on load, we want to grab all of the rooms we are in
* put them in a map
*
* fetch more audio clips, fire off async function to fetch more and add to the room map
*
* Socket Rooms:
* - all people online for a line
* - all tuned in folks on a line...have it selected or toggle tuned
*
* Socket Events:
* - someone connected
* - someone tuned in
* - someone started broadcasting
* - someone stopped broadcasting
*
* - someone disconnected...take them out of the necessary lists
* - someone left x room
* - someone joined x room
*
* - someone added me to line
*
* - someone went into flow state their status
*
* Socket Emissions:
* - join a line
* - tune into a line
* - send audio clip
* - create a line -> send to specific people
*
* REST endpoints:
* - toggle tune or untoggle tune
* - fetch content blocks for line history - react query
*/
// TODO: many of this can be done in a HOC like <Terminal /> but it doesn't matter, they both just
// re-render, just make sure to pass in lighter props to children like main panel or lineRow
export function TerminalProvider({ children }: { children: React.ReactChild }) {
const { rooms } = useRooms();
const { user } = useAuth();
const { $ws } = useSockets();
const [roomMap, updateRoomMap] = useImmer<LineIdToMasterLine>({});
const { desktopMode } = useElectron();
const [selectedLineId, setSelectedLineId] = useState<string>();
const [moveLineState, moveLine] = useAsyncFn(updateLineMemberState);
const [showNewChannelForm, setShowNewChannelForm] = useState<boolean>(false);
/** All line listeners */
useEffect(() => {
// when me or anyone just initially connects to line
$ws.on(ServerResponseChannels.SOMEONE_CONNECTED_TO_LINE, (res: SomeoneConnectedResponse) => {
console.log(`${res.userId} connected to room ${res.lineId}`);
updateRoomMap((draft) => {
if (!draft[res.lineId]) {
toast.error('there was a problem updating rooms!!!');
return;
}
draft[res.lineId].connectedMemberIds = res.allUsers;
});
});
// someone tuning in, including perhaps me | either toggled in or just temporary
$ws.on(ServerResponseChannels.SOMEONE_TUNED_INTO_LINE, (res: SomeoneTunedResponse) => {
console.log(`${res.userId} tuned into line ${res.lineId}`);
updateRoomMap((draft) => {
if (!draft[res.lineId]) {
toast.error('there was a problem updating rooms!!!');
return;
}
draft[res.lineId].tunedInMemberIds = res.allUsers;
});
});
$ws.on(
ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE,
(res: SomeoneUntunedFromLineResponse) => {
console.log(`${res.userId} untuned from ${res.lineId}`);
updateRoomMap((draft) => {
if (!draft[res.lineId]) {
toast.error('there was a problem updating rooms!!!');
return;
}
if (draft[res.lineId].tunedInMemberIds) {
draft[res.lineId].tunedInMemberIds = draft[res.lineId].tunedInMemberIds.filter(
(userId) => userId !== res.userId,
);
}
});
},
);
// remove them from the line connected list and tuned list if they are there
$ws.on(
ServerResponseChannels.SOMEONE_DISCONNECTED_FROM_LINE,
(res: SomeoneDisconnectedResponse) => {
updateRoomMap((draft) => {
if (!draft[res.lineId]) {
toast.error('there was a problem updating rooms!!!');
return;
}
draft[res.lineId].connectedMemberIds = draft[res.lineId].connectedMemberIds?.filter(
(userId) => userId !== res.userId,
);
draft[res.lineId].tunedInMemberIds = draft[res.lineId].tunedInMemberIds?.filter(
(userId) => userId !== res.userId,
);
});
},
);
$ws.on(
ServerResponseChannels.SOMEONE_STARTED_BROADCASTING,
(res: UserStartedBroadcastingResponse) => {
console.log(`${res.userId} is starting to broadcast in ${res.lineId}`);
updateRoomMap((draft) => {
if (!draft[res.lineId]) {
toast.error('there was a problem updating rooms!!!');
return;
}
if (draft[res.lineId].currentBroadcastersUserIds) {
draft[res.lineId].currentBroadcastersUserIds.push(res.userId);
} else {
draft[res.lineId].currentBroadcastersUserIds = [res.userId];
}
});
},
);
$ws.on(
ServerResponseChannels.SOMEONE_STOPPED_BROADCASTING,
(res: UserStoppedBroadcastingResponse) => {
console.log(`${res.userId} is STOPPED BROADCASTING in ${res.lineId}`);
updateRoomMap((draft) => {
if (!draft[res.lineId]) {
toast.error('there was a problem updating rooms!!!');
return;
}
if (draft[res.lineId].currentBroadcastersUserIds) {
draft[res.lineId].currentBroadcastersUserIds = draft[
res.lineId
].currentBroadcastersUserIds.filter((userId) => userId !== res.userId);
}
});
},
);
return () => {
// ?perhaps only remove specific ones?
// !this will remove all listeners across the app and we want it to?
$ws.removeAllListeners();
};
}, [$ws, updateRoomMap]);
const handleConnectToLine = useCallback(
(lineId: string) => {
$ws.emit(ServerRequestChannels.CONNECT_TO_LINE, new ConnectToLineRequest(lineId));
},
[$ws],
);
const handleTuneIntoLine = useCallback(
(lineId: string) => {
$ws.emit(ServerRequestChannels.TUNE_INTO_LINE, new TuneToLineRequest(lineId));
},
[$ws],
);
const handleUntuneFromLine = useCallback(
(lineId: string) => {
$ws.emit(ServerRequestChannels.UNTUNE_FROM_LINE, new UntuneFromLineRequest(lineId));
},
[$ws],
);
// converts the initial rooms fetch to a map
useEffect(() => {
if (rooms.value?.data?.masterLines?.length > 0) {
updateRoomMap((draft) => {
rooms.value.data.masterLines.forEach((masterLine) => {
const lineId = masterLine.lineDetails._id.toString();
draft[lineId] = masterLine;
handleConnectToLine(lineId);
if (masterLine.currentUserMember.state === LineMemberState.TUNED) {
handleTuneIntoLine(lineId);
}
});
});
}
}, [rooms.value, updateRoomMap, handleConnectToLine, handleTuneIntoLine]);
// "subscribe" to a channel
const handleAddChannel = useCallback(
(channelId: string) => {
// get the details of the channel
// get my association with it
// get other members in it
// add to map
// connect/join the socket room for it
},
[handleConnectToLine, updateRoomMap],
);
// persist whether I want it toggle tuned or not
const handleUpdateLineMemberState = useCallback(
(lineId: string, newState: LineMemberState) => {
moveLine(new UpdateLineMemberState(newState), lineId)
.then((_res) => {
updateRoomMap((draft) => {
draft[lineId].currentUserMember.state = newState;
});
})
.catch((error) => {
toast.error('problem in updating line member state');
console.error(error);
});
},
[moveLine, updateRoomMap],
);
/**
* selection globally
* tune into socket room and tell everyone else
*/
const handleSelectLine = useCallback(
(newLineIdToSelect: string) => {
setSelectedLineId((prevLineId) => {
if (newLineIdToSelect === prevLineId) {
return prevLineId;
}
// untune from the last line if it was just a temporary tuned one
if (roomMap[prevLineId]?.currentUserMember.state === LineMemberState.INBOX) {
handleUntuneFromLine(prevLineId);
}
// tune in if not already tuned into this line
if (!roomMap[newLineIdToSelect].tunedInMemberIds?.includes(user._id.toString())) {
handleTuneIntoLine(newLineIdToSelect);
}
return newLineIdToSelect;
});
setShowNewChannelForm(false);
},
[
setSelectedLineId,
handleUntuneFromLine,
roomMap,
user,
handleTuneIntoLine,
setShowNewChannelForm,
],
);
const handleShowNewChannelForm = useCallback(
(showOrHide: 'show' | 'hide' = 'show') => {
setSelectedLineId(undefined);
setShowNewChannelForm(showOrHide === 'show');
},
[setShowNewChannelForm, setSelectedLineId],
);
// handle shortcuts
const clearMind = useCallback(() => {
setSelectedLineId((prevLineId) => {
// untune from the last line if it was just a temporary tuned one
if (roomMap[prevLineId]?.currentUserMember.state === LineMemberState.INBOX) {
handleUntuneFromLine(prevLineId);
}
return undefined;
});
setShowNewChannelForm(false);
}, [setSelectedLineId, setShowNewChannelForm, handleUntuneFromLine, roomMap]);
useKeyPressEvent('Escape', clearMind);
// todo: sort based on content blocks and my last activity date
const allChannels = useMemo(() => {
let channels: MasterLineData[] = Object.values(roomMap);
channels = channels.map((currChann) => Object.assign({}, currChann, MasterLineData));
if (desktopMode === 'overlayOnly') {
channels = channels.filter((currentChannel) =>
currentChannel.tunedInMemberIds?.includes(user._id.toString()),
);
}
channels.sort((channelA, channelB) => {
if (
channelA.currentUserMember.state === LineMemberState.TUNED &&
channelB.currentUserMember.state === LineMemberState.INBOX
)
return -1;
if (
channelB.currentUserMember.state === LineMemberState.TUNED &&
channelA.currentUserMember.state === LineMemberState.INBOX
)
return 1;
if (channelA.lineDetails.createdDate > channelB.lineDetails.createdDate) return 1;
// sort also by activity
return -1;
});
channels.forEach((currChannel) => {
currChannel.isUserTunedIn = currChannel.tunedInMemberIds?.includes(user._id.toString())
? true
: false;
currChannel.isUserToggleTuned = currChannel.currentUserMember.state === LineMemberState.TUNED;
const allMembers: string[] = [];
const allMembersWithoutMe: string[] = [];
const tunedMembers: string[] = [];
const broadcastMembers: string[] = [];
const untunedMembers: string[] = [];
// ?don't add in my image as that's useless contextually?
if (user.picture) allMembers.push(user.picture);
currChannel.otherUserObjects?.forEach((otherUser) => {
if (otherUser.picture) {
allMembers.push(otherUser.picture);
allMembersWithoutMe.push(otherUser.picture);
if (currChannel.tunedInMemberIds?.includes(otherUser._id.toString())) {
tunedMembers.push(otherUser.picture);
return;
}
if (currChannel.currentBroadcastersUserIds?.includes(otherUser._id.toString())) {
broadcastMembers.push(otherUser.picture);
return;
}
untunedMembers.push(otherUser.picture);
}
});
currChannel.profilePictures = {
untunedMembers,
allMembers,
tunedMembers,
broadcastMembers,
allMembersWithoutMe,
};
});
return channels;
}, [roomMap, desktopMode, user]);
const tunedChannelsCount = useMemo(
() =>
allChannels?.filter(
(currChannel) => currChannel.currentUserMember.state === LineMemberState.TUNED,
)?.length,
[allChannels],
);
// !Caution: the roommap won't have the additional properties as allChannels does
return (
<TerminalContext.Provider
value={{
roomsMap: roomMap,
allChannels,
tunedChannelsCount,
handleSelectLine,
selectedLineId,
handleUpdateLineMemberState,
showNewChannelForm,
handleShowNewChannelForm,
}}
>
<StreamProvider>
<>
<div className="flex flex-row flex-1 h-full w-full">
<SidePanel />
{desktopMode === 'mainApp' && <MainPanel />}
</div>
{children}
</>
</StreamProvider>
</TerminalContext.Provider>
);
}
export default function useTerminalProvider() {
return useContext(TerminalContext);
}
@@ -0,0 +1 @@
// handle the flow state and other things
@@ -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();
+5 -8
View File
@@ -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;