throwing away bunch of garbage
This commit is contained in:
@@ -1,16 +1,9 @@
|
|||||||
import express, { Application, Request, Response } from 'express';
|
import express, { Application, Request, Response } from 'express';
|
||||||
|
|
||||||
import GetAllSocketClients from '@nirvana/core/sockets/getAllActiveSocketClients';
|
|
||||||
import InitializeWs from './services/socket.service';
|
import InitializeWs from './services/socket.service';
|
||||||
import { NextFunction } from 'express';
|
import { NextFunction } from 'express';
|
||||||
import NirvanaResponse from '@nirvana/core/responses/nirvanaResponse';
|
import NirvanaResponse from '@nirvana/core/responses/nirvanaResponse';
|
||||||
import ReceiveSignal from '../core/sockets/receiveSignal';
|
|
||||||
import SendSignal from '@nirvana/core/sockets/sendSignal';
|
|
||||||
import SocketChannels from '@nirvana/core/sockets/channels';
|
|
||||||
import { UserService } from './services/user.service';
|
|
||||||
import { UserStatus } from '@nirvana/core/models';
|
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import getLineRoutes from './routes/line';
|
|
||||||
import getSearchRoutes from './routes/search';
|
import getSearchRoutes from './routes/search';
|
||||||
import getUserRoutes from './routes/user';
|
import getUserRoutes from './routes/user';
|
||||||
import morgan from 'morgan';
|
import morgan from 'morgan';
|
||||||
@@ -41,7 +34,7 @@ app.use('/api/status', (req: Request, res: Response) => {
|
|||||||
|
|
||||||
app.use('/api/user', getUserRoutes());
|
app.use('/api/user', getUserRoutes());
|
||||||
app.use('/api/search', getSearchRoutes());
|
app.use('/api/search', getSearchRoutes());
|
||||||
app.use('/api/lines', getLineRoutes());
|
// app.use('/api/conversations', getConversationRoutes());
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8080;
|
const PORT = process.env.PORT || 8080;
|
||||||
const server = app.listen(PORT, () =>
|
const server = app.listen(PORT, () =>
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
import { JwtClaims, authCheck } from '../middleware/auth';
|
|
||||||
import { Line, LineMember, LineMemberState } from '@nirvana/core/models/line.model';
|
|
||||||
import express, { Application, Request, Response } from 'express';
|
|
||||||
|
|
||||||
import Content from '@nirvana/core/models/content.model';
|
|
||||||
import CreateLineRequest from '@nirvana/core/requests/createLine.request';
|
|
||||||
import GetConversationDetailsResponse from '@nirvana/core/responses/getConversationDetails.response';
|
|
||||||
import GetDmConversationByOtherUserIdResponse from '@nirvana/core/responses/getDmConversationByOtherUserId.response';
|
|
||||||
import GetUserLinesResponse from '@nirvana/core/responses/getUserLines.response';
|
|
||||||
import { LineService } from '../services/line.service';
|
|
||||||
import MasterLineData from '@nirvana/core/models/masterLineData.model';
|
|
||||||
import NirvanaResponse from '@nirvana/core/responses/nirvanaResponse';
|
|
||||||
import { ObjectId } from 'mongodb';
|
|
||||||
import Relationship from '@nirvana/core/models/relationship.model';
|
|
||||||
import { User } from '@nirvana/core/models/user.model';
|
|
||||||
import { UserService } from '../services/user.service';
|
|
||||||
import { collections } from '../services/database.service';
|
|
||||||
import UpdateLineMemberState from '@nirvana/core/requests/updateLineMemberState.request';
|
|
||||||
|
|
||||||
export default function getLineRoutes() {
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.use(express.json());
|
|
||||||
|
|
||||||
// get data for a one on one conversation
|
|
||||||
// router.get("/:otherUserGoogleUserId", authCheck, getConversationDetails);
|
|
||||||
|
|
||||||
// create a line
|
|
||||||
router.post('/', authCheck, createLine);
|
|
||||||
|
|
||||||
// get all of user's lines
|
|
||||||
router.get('/', authCheck, getUserLines);
|
|
||||||
|
|
||||||
// toggle tune into a line
|
|
||||||
router.post('/:lineId/state', authCheck, updateLineMemberState);
|
|
||||||
|
|
||||||
// get conversation between user and other user
|
|
||||||
router.get('/dm/:otherUserId', authCheck, getDmByOtherUserId);
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDmByOtherUserId(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const { otherUserId } = req.params;
|
|
||||||
const userInfo = res.locals.userInfo as JwtClaims;
|
|
||||||
|
|
||||||
console.log(otherUserId);
|
|
||||||
|
|
||||||
// check db for lines between me and this other person
|
|
||||||
// if there is one, then return it with 200 status
|
|
||||||
// else return it with custom status that frontend will read
|
|
||||||
|
|
||||||
res.status(200).json();
|
|
||||||
return;
|
|
||||||
|
|
||||||
res.status(205).json('no such conversation between you two');
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createLine(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const reqObj: CreateLineRequest = req.body as CreateLineRequest;
|
|
||||||
console.log(req.body);
|
|
||||||
|
|
||||||
if (!reqObj?.otherMemberIds.length) {
|
|
||||||
res.status(400).json('must provide member Ids');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInfo = res.locals.userInfo as JwtClaims;
|
|
||||||
|
|
||||||
const newLine = new Line(
|
|
||||||
new ObjectId(userInfo.userId),
|
|
||||||
reqObj.lineName ?? undefined,
|
|
||||||
new Date(),
|
|
||||||
new Date(),
|
|
||||||
new ObjectId(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: validate that users exists before creating line members
|
|
||||||
const lineMembers: LineMember[] =
|
|
||||||
reqObj.otherMemberIds.map((memId) => {
|
|
||||||
const newLineMember = new LineMember(
|
|
||||||
newLine._id!,
|
|
||||||
new ObjectId(memId),
|
|
||||||
LineMemberState.INBOX,
|
|
||||||
);
|
|
||||||
|
|
||||||
return newLineMember;
|
|
||||||
}) ?? [];
|
|
||||||
|
|
||||||
lineMembers.push(
|
|
||||||
new LineMember(newLine._id!, new ObjectId(userInfo.userId), LineMemberState.INBOX),
|
|
||||||
);
|
|
||||||
|
|
||||||
const transactionResult = await LineService.createLine(newLine, lineMembers);
|
|
||||||
|
|
||||||
transactionResult
|
|
||||||
? res.status(200).json(new NirvanaResponse(newLine))
|
|
||||||
: res.status(400).json(new NirvanaResponse(undefined, new Error('unable to create line')));
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateLineMemberState(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const userInfo = res.locals.userInfo as JwtClaims;
|
|
||||||
const { lineId } = req.params;
|
|
||||||
const request = req.body as UpdateLineMemberState;
|
|
||||||
|
|
||||||
// TODO: validation to check if user is actually a member of the line
|
|
||||||
|
|
||||||
const result = await LineService.updateLineMemberState(
|
|
||||||
lineId,
|
|
||||||
userInfo.userId,
|
|
||||||
request.newState,
|
|
||||||
);
|
|
||||||
|
|
||||||
return result?.ok
|
|
||||||
? res.status(200).json(new NirvanaResponse("successfully updated line member's state"))
|
|
||||||
: res
|
|
||||||
.status(400)
|
|
||||||
.json(new NirvanaResponse(undefined, new Error('not updated...something went wrong')));
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json(new NirvanaResponse(undefined, error as Error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUserLines(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const userInfo = res.locals.userInfo as JwtClaims;
|
|
||||||
|
|
||||||
// get all of user's lineMember entries
|
|
||||||
const userLineMembers = await LineService.getLineMembersByUserId(userInfo.userId);
|
|
||||||
|
|
||||||
if (!userLineMembers?.length) {
|
|
||||||
const resObj = new GetUserLinesResponse([]);
|
|
||||||
|
|
||||||
res.json(new NirvanaResponse(resObj, undefined, 'this user is not in any lines'));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineIds = userLineMembers?.map((lineMember) => lineMember.lineId) ?? [];
|
|
||||||
|
|
||||||
// this will include the current user lineMember association to the line
|
|
||||||
const allLineMembers = await LineService.getLineMembersInLines(lineIds);
|
|
||||||
|
|
||||||
const allLinesUsersIds: ObjectId[] = [];
|
|
||||||
allLineMembers?.map((currentLineMember) => {
|
|
||||||
if (currentLineMember?.userId) allLinesUsersIds.push(currentLineMember.userId);
|
|
||||||
}) ?? [];
|
|
||||||
|
|
||||||
// get all users relevant here
|
|
||||||
const allRelevantUsers = await UserService.getUsersByIds(allLinesUsersIds);
|
|
||||||
|
|
||||||
// get all lines from the list of relevant lines
|
|
||||||
const lines = (await LineService.getLinesByIds(lineIds)) ?? [];
|
|
||||||
|
|
||||||
const masterLines: MasterLineData[] = [];
|
|
||||||
|
|
||||||
// TODO: get the latest audio blocks for this line...maybe like today and yesterday or by block count
|
|
||||||
|
|
||||||
lines.map((currentLine) => {
|
|
||||||
let associatedLineMembersForLine: LineMember[] = [];
|
|
||||||
|
|
||||||
// get all of the line members for this Line
|
|
||||||
// make sure that we don't add line member if it's the current user
|
|
||||||
allLineMembers?.map((lineMember) => {
|
|
||||||
if (currentLine._id) {
|
|
||||||
if (lineMember.lineId.equals(currentLine._id))
|
|
||||||
associatedLineMembersForLine.push(lineMember);
|
|
||||||
}
|
|
||||||
}) ?? [];
|
|
||||||
|
|
||||||
// get the user lineMember assoc out of the list of the lineMembers for this line
|
|
||||||
const userLineMember = associatedLineMembersForLine?.find(
|
|
||||||
(currentLineMemberForLine) =>
|
|
||||||
currentLineMemberForLine.userId.toString() === userInfo.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userLineMember) {
|
|
||||||
// take out the current user from the "other" line members list now
|
|
||||||
|
|
||||||
associatedLineMembersForLine = associatedLineMembersForLine.filter(
|
|
||||||
(currentLineMember) => currentLineMember.userId.toString() !== userInfo.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// get the user objects for all of the other members
|
|
||||||
const otherUsers: User[] = [];
|
|
||||||
associatedLineMembersForLine.forEach((currentLineMember) => {
|
|
||||||
const foundUserObject = allRelevantUsers?.find((currentUser) =>
|
|
||||||
currentUser._id?.equals(currentLineMember.userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (foundUserObject) otherUsers.push(foundUserObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
masterLines.push(
|
|
||||||
new MasterLineData(currentLine, userLineMember, associatedLineMembersForLine, otherUsers),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const resObj = new GetUserLinesResponse(masterLines);
|
|
||||||
|
|
||||||
res.json(new NirvanaResponse<GetUserLinesResponse>(resObj));
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { GoogleUserInfo, User } from '@nirvana/core/models';
|
|
||||||
import { JwtClaims, authCheck } from '../middleware/auth';
|
import { JwtClaims, authCheck } from '../middleware/auth';
|
||||||
|
import User, { GoogleUserInfo, UserStatus } from '@nirvana/core/models/user.model';
|
||||||
import express, { Application, Request, Response } from 'express';
|
import express, { Application, Request, Response } from 'express';
|
||||||
|
|
||||||
import LoginResponse from '../../core/responses/login.response';
|
import LoginResponse from '../../core/responses/login.response';
|
||||||
@@ -8,7 +8,6 @@ import { ObjectID } from 'bson';
|
|||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import UserDetailsResponse from '../../core/responses/userDetails.response';
|
import UserDetailsResponse from '../../core/responses/userDetails.response';
|
||||||
import { UserService } from '../services/user.service';
|
import { UserService } from '../services/user.service';
|
||||||
import { UserStatus } from '../../core/models/user.model';
|
|
||||||
import { collections } from '../services/database.service';
|
import { collections } from '../services/database.service';
|
||||||
import environmentVariables from '../config/config';
|
import environmentVariables from '../config/config';
|
||||||
|
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
import {
|
|
||||||
Line,
|
|
||||||
LineMember,
|
|
||||||
LineMemberState,
|
|
||||||
} from "@nirvana/core/models/line.model";
|
|
||||||
import { client, collections } from "./database.service";
|
|
||||||
|
|
||||||
import NirvanaResponse from "@nirvana/core/responses/nirvanaResponse";
|
|
||||||
import { ObjectId } from "mongodb";
|
|
||||||
|
|
||||||
export class LineService {
|
|
||||||
static async getLineByOtherUserId(otherUserId: ObjectId) {
|
|
||||||
// get all of the conversations for this user that have exactly two conversation members
|
|
||||||
// get all of the conversationMembers for this user
|
|
||||||
// get all of the conversations for this user
|
|
||||||
// get all of the conversations
|
|
||||||
// const query = { googleId: userId };
|
|
||||||
// const res = await collections.users?.findOne(query);
|
|
||||||
// // exists
|
|
||||||
// if (res?._id) {
|
|
||||||
// return res as User;
|
|
||||||
// }
|
|
||||||
// return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getLinesByIds(convoIds: ObjectId[]) {
|
|
||||||
const query = { _id: { $in: convoIds } };
|
|
||||||
|
|
||||||
const convosRes = await collections.lines?.find(query).toArray();
|
|
||||||
|
|
||||||
// exists
|
|
||||||
if (convosRes?.length) {
|
|
||||||
return convosRes as Line[];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getLineMembersByUserId(userId: string) {
|
|
||||||
const query = { userId: new ObjectId(userId) };
|
|
||||||
|
|
||||||
const convoMembersRes = await collections.lineMembers
|
|
||||||
?.find(query)
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
// exists
|
|
||||||
if (convoMembersRes?.length) {
|
|
||||||
return convoMembersRes as LineMember[];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get all of the members associated to the given list of lines */
|
|
||||||
static async getLineMembersInLines(lineIds: ObjectId[]) {
|
|
||||||
const query = { lineId: { $in: lineIds } };
|
|
||||||
|
|
||||||
const lineMembersRes = await collections.lineMembers?.find(query).toArray();
|
|
||||||
|
|
||||||
// exists
|
|
||||||
if (lineMembersRes?.length) {
|
|
||||||
return lineMembersRes as LineMember[];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async createLine(line: Line, lineMembers: LineMember[]) {
|
|
||||||
const session = client.startSession();
|
|
||||||
try {
|
|
||||||
const transactionResults = await session.withTransaction(async () => {
|
|
||||||
// todo: check if convoMembers userId's actually exist
|
|
||||||
|
|
||||||
const insertLineRes = await collections.lines?.insertOne(line);
|
|
||||||
if (!insertLineRes?.insertedId) {
|
|
||||||
await session.abortTransaction();
|
|
||||||
console.error("failed to create line");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertConvoMembersRes = await collections.lineMembers?.insertMany(
|
|
||||||
lineMembers
|
|
||||||
);
|
|
||||||
if (!insertConvoMembersRes?.insertedCount) {
|
|
||||||
await session.abortTransaction();
|
|
||||||
console.error("failed to create line members");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("success");
|
|
||||||
return insertConvoMembersRes;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(transactionResults);
|
|
||||||
|
|
||||||
return "success";
|
|
||||||
|
|
||||||
// if (transactionResults) {
|
|
||||||
// console.log("The convo was successfully created.");
|
|
||||||
// return transactionResults;
|
|
||||||
// } else {
|
|
||||||
// console.log("The convo was intentionally aborted.");
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
} catch (e) {
|
|
||||||
console.log(
|
|
||||||
"The transaction was aborted due to an unexpected error: " + e
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
await session.endSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async updateLineMemberState(
|
|
||||||
lineId: string,
|
|
||||||
userId: string,
|
|
||||||
newState: LineMemberState
|
|
||||||
) {
|
|
||||||
const query = {
|
|
||||||
lineId: new ObjectId(lineId),
|
|
||||||
userId: new ObjectId(userId),
|
|
||||||
};
|
|
||||||
const updateSet = { $set: { state: newState, lastVisitDate: new Date() } };
|
|
||||||
|
|
||||||
const updateRes = await collections.lineMembers?.findOneAndUpdate(
|
|
||||||
query,
|
|
||||||
updateSet
|
|
||||||
);
|
|
||||||
|
|
||||||
return updateRes;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async updateLineMemberVisitDate(lineId: string, userId: string) {
|
|
||||||
const query = { lineId, userId: new ObjectId(userId) };
|
|
||||||
const updateSet = { $set: { lastVisitDate: new Date() } };
|
|
||||||
|
|
||||||
const updateRes = await collections.lineMembers?.findOneAndUpdate(
|
|
||||||
query,
|
|
||||||
updateSet
|
|
||||||
);
|
|
||||||
|
|
||||||
return updateRes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,15 +18,7 @@ import {
|
|||||||
UserStoppedBroadcastingResponse,
|
UserStoppedBroadcastingResponse,
|
||||||
} from '@nirvana/core/sockets/channels';
|
} from '@nirvana/core/sockets/channels';
|
||||||
|
|
||||||
import GetAllSocketClients from '@nirvana/core/sockets/getAllActiveSocketClients';
|
|
||||||
import { JwtClaims } from '../middleware/auth';
|
import { JwtClaims } from '../middleware/auth';
|
||||||
import { LineMemberState } from '@nirvana/core/models/line.model';
|
|
||||||
import { LineService } from './line.service';
|
|
||||||
import ReceiveSignal from '@nirvana/core/sockets/receiveSignal';
|
|
||||||
import SendSignal from '@nirvana/core/sockets/sendSignal';
|
|
||||||
import { UserService } from './user.service';
|
|
||||||
import { UserStatus } from '@nirvana/core/models/user.model';
|
|
||||||
import { client } from './database.service';
|
|
||||||
import environmentVariables from '../config/config';
|
import environmentVariables from '../config/config';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { GoogleUserInfo, User } from "@nirvana/core/models";
|
import User, { GoogleUserInfo } from '@nirvana/core/models/user.model';
|
||||||
|
|
||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from 'mongodb';
|
||||||
import { UserStatus } from "../../core/models/user.model";
|
import { UserStatus } from '../../core/models/user.model';
|
||||||
import axios from "axios";
|
import axios from 'axios';
|
||||||
import { collections } from "./database.service";
|
import { collections } from './database.service';
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
static async getUserById(userId: string) {
|
static async getUserById(userId: string) {
|
||||||
@@ -62,11 +62,11 @@ export class UserService {
|
|||||||
// based on index defined in Mongo atlas
|
// based on index defined in Mongo atlas
|
||||||
const query = {
|
const query = {
|
||||||
$search: {
|
$search: {
|
||||||
index: "basic user search",
|
index: 'basic user search',
|
||||||
text: {
|
text: {
|
||||||
query: searchQuery,
|
query: searchQuery,
|
||||||
path: {
|
path: {
|
||||||
wildcard: "*",
|
wildcard: '*',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -95,13 +95,9 @@ export class UserService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getGoogleUserInfoWithAccessToken(
|
static async getGoogleUserInfoWithAccessToken(accessToken: string): Promise<GoogleUserInfo> {
|
||||||
accessToken: string
|
|
||||||
): Promise<GoogleUserInfo> {
|
|
||||||
return (
|
return (
|
||||||
await axios.get(
|
await axios.get(`https://www.googleapis.com/oauth2/v1/userinfo?access_token=${accessToken}`)
|
||||||
`https://www.googleapis.com/oauth2/v1/userinfo?access_token=${accessToken}`
|
|
||||||
)
|
|
||||||
).data;
|
).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,38 @@
|
|||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
export default class Content {
|
export interface IContent {
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
creatorUserId: string;
|
||||||
|
contentUrl: string; // remote resource of file/media
|
||||||
|
createdDate: Date;
|
||||||
|
|
||||||
|
// visitCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// ? persist length of clip for easier viewing for others...
|
||||||
|
// ?they have to load it anyway and will get metadata anyway?
|
||||||
|
export class ContentBlock implements IContent {
|
||||||
constructor(
|
constructor(
|
||||||
public relationshipId: string, // if it's a one on one, this will be the relationship Id
|
public creatorUserId: string,
|
||||||
public sentDate: Date,
|
|
||||||
public contentUrl: string,
|
public contentUrl: string,
|
||||||
public contentData: string,
|
|
||||||
public contentType: ContentType,
|
public contentType: ContentType,
|
||||||
public _id?: ObjectId,
|
public blobType: string,
|
||||||
public listenedDate?: Date
|
|
||||||
|
public createdDate = new Date(),
|
||||||
|
|
||||||
|
public id?: string,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContentType {
|
export enum ContentType {
|
||||||
link = "LINK",
|
audio = 'audio',
|
||||||
audioClip = "AUDIO_CLIP",
|
link = 'link',
|
||||||
text = "TEXT",
|
image = 'image',
|
||||||
|
code = 'code',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUrlImage(url: string) {
|
||||||
|
return url.match(/\.(jpeg|jpg|gif|png)$/) != null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import User from './user.model';
|
||||||
|
|
||||||
|
export default class Conversation {
|
||||||
|
constructor(
|
||||||
|
public createdByUserId: string,
|
||||||
|
|
||||||
|
public memberIdsList: string[],
|
||||||
|
|
||||||
|
public members: ConversationMember[],
|
||||||
|
|
||||||
|
public userCache: User[],
|
||||||
|
|
||||||
|
public name: string | null = null,
|
||||||
|
|
||||||
|
public lastUpdatedDate = new Date(),
|
||||||
|
|
||||||
|
public createdDate = new Date(),
|
||||||
|
|
||||||
|
public membersInRoom: string[] = [],
|
||||||
|
|
||||||
|
public id?: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConversationMember {
|
||||||
|
constructor(
|
||||||
|
public userId: string, // NOTE: serves as the user ID
|
||||||
|
|
||||||
|
public role: MemberRole,
|
||||||
|
public memberState: MemberState,
|
||||||
|
|
||||||
|
public joinedDate = new Date(),
|
||||||
|
public lastActiveDate?: Date, // when I last spoke in the conversation or contributed with some content
|
||||||
|
public lastVisitDate?: Date, // when I last clicked into a conversation
|
||||||
|
public lastFetchDate?: Date, // for the use of long polling in the future
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MemberRole {
|
||||||
|
admin = 'admin',
|
||||||
|
regular = 'regular',
|
||||||
|
}
|
||||||
|
export enum MemberState {
|
||||||
|
priority = 'priority',
|
||||||
|
inbox = 'inbox',
|
||||||
|
// deleted = "deleted"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ObjectId } from "mongodb";
|
|
||||||
|
|
||||||
export class GoogleUserInfo {
|
|
||||||
constructor(
|
|
||||||
public id: string,
|
|
||||||
public email: string,
|
|
||||||
public verifiedEmail: boolean,
|
|
||||||
public name: string,
|
|
||||||
public given_name: string,
|
|
||||||
public family_name: string,
|
|
||||||
public picture: string,
|
|
||||||
public locale: string
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./googleUserInfo.model";
|
|
||||||
export * from "./user.model";
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { ObjectId } from "mongodb";
|
|
||||||
|
|
||||||
export class Line {
|
|
||||||
constructor(
|
|
||||||
public createdByUserId: ObjectId,
|
|
||||||
public name?: string,
|
|
||||||
public createdDate: Date = new Date(),
|
|
||||||
public lastUpdatedDate: Date = new Date(),
|
|
||||||
public _id?: ObjectId
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LineMember {
|
|
||||||
// the last time that the user visited the line
|
|
||||||
// for new activity monitoring
|
|
||||||
lastVisitDate?: Date;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
// unique constraint
|
|
||||||
public lineId: ObjectId,
|
|
||||||
public userId: ObjectId,
|
|
||||||
|
|
||||||
public state: LineMemberState,
|
|
||||||
|
|
||||||
public createdDate: Date = new Date(),
|
|
||||||
public _id?: ObjectId
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LineMemberState {
|
|
||||||
// these states will be for later when we want an inbox and such
|
|
||||||
// INVITED = "INVITED", // user gets this in their request inbox
|
|
||||||
|
|
||||||
// ARCHIVED = "ARCHIVED", // user no longer wants anything to do with convo
|
|
||||||
|
|
||||||
INBOX = "INBOX", // user decided to join this conversation after being invited...for now things just land in inbox anyway
|
|
||||||
TUNED = "TUNED", // user upgraded priority of this and now is toggle tuned in live to convo
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Line, LineMember } from './line.model';
|
|
||||||
|
|
||||||
import AudioClip from './audioClip.model';
|
|
||||||
import { ObjectId } from 'mongodb';
|
|
||||||
import { User } from './user.model';
|
|
||||||
|
|
||||||
// why do we have separate full objects being sent?
|
|
||||||
// speed...I'm developing full stack and I just want all of the data and don't want to change this model
|
|
||||||
// repeatedly and trace data back and forth
|
|
||||||
export default class MasterLineData {
|
|
||||||
// NOTE: these properties should be kept optional cuzz default values won't show up for
|
|
||||||
// all clients who are casting response objects
|
|
||||||
|
|
||||||
// not really necessary, but clients can know all connected folks
|
|
||||||
// -> if they wanted to get the feeling of people being right there
|
|
||||||
connectedMemberIds?: string[];
|
|
||||||
|
|
||||||
// all of the current session tuned in folks for user to see
|
|
||||||
// -> use as source of truth whether or not I am tuned in or not for UI to avoid confusion
|
|
||||||
// ->
|
|
||||||
tunedInMemberIds?: string[];
|
|
||||||
|
|
||||||
// list of user Ids of everyone who is buzzing in this line
|
|
||||||
// -> all connected line members be able to show this in the right activity section of the lineRow
|
|
||||||
currentBroadcastersUserIds?: string[];
|
|
||||||
|
|
||||||
profilePictures?: {
|
|
||||||
allMembers: string[];
|
|
||||||
allMembersWithoutMe: string[];
|
|
||||||
|
|
||||||
untunedMembers: string[];
|
|
||||||
|
|
||||||
tunedMembers: string[];
|
|
||||||
broadcastMembers: string[];
|
|
||||||
} = undefined;
|
|
||||||
isUserTunedIn: boolean = false;
|
|
||||||
isUserToggleTuned: boolean = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
// full line object
|
|
||||||
public lineDetails: Line,
|
|
||||||
|
|
||||||
// requesting user's association to the line
|
|
||||||
public currentUserMember: LineMember,
|
|
||||||
|
|
||||||
// all other members in the convo as well as their user object to see the member details
|
|
||||||
public otherMembers?: LineMember[],
|
|
||||||
|
|
||||||
public otherUserObjects?: User[] /**public audioClips: AudioClip[] = [], // public media: Media[] */,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { ObjectId } from "mongodb";
|
|
||||||
|
|
||||||
// some sort of contact or friend model
|
|
||||||
|
|
||||||
export default class Relationship {
|
|
||||||
constructor(
|
|
||||||
public senderUserId: string, // sender as the one to initiate the relationship
|
|
||||||
public receiverUserId: string,
|
|
||||||
public state: RelationshipState,
|
|
||||||
public createdDate: Date = new Date(),
|
|
||||||
public lastUpdatedDate: Date = new Date(),
|
|
||||||
|
|
||||||
public _id?: ObjectId
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RelationshipState {
|
|
||||||
PENDING = "PENDING",
|
|
||||||
ACTIVE = "ACTIVE",
|
|
||||||
BLOCKED = "BLOCKED",
|
|
||||||
DENIED = "DENIED",
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
export class User {
|
export default class User {
|
||||||
constructor(
|
constructor(
|
||||||
public googleId: string, // our Google id that every google user has unique that we are going to use for now
|
public googleId: string,
|
||||||
public email: string,
|
public email: string,
|
||||||
|
|
||||||
public name: string,
|
public name: string,
|
||||||
@@ -17,12 +17,25 @@ export class User {
|
|||||||
// additional properties specific to our users collection
|
// additional properties specific to our users collection
|
||||||
public status?: UserStatus,
|
public status?: UserStatus,
|
||||||
public lastUpdatedDate?: Date,
|
public lastUpdatedDate?: Date,
|
||||||
public _id?: ObjectId
|
public _id?: ObjectId,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserStatus {
|
export enum UserStatus {
|
||||||
ONLINE = "ONLINE",
|
ONLINE = 'ONLINE',
|
||||||
OFFLINE = "OFFLINE",
|
OFFLINE = 'OFFLINE',
|
||||||
FLOW_STATE = "FLOW_STATE",
|
FLOW_STATE = 'FLOW_STATE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GoogleUserInfo {
|
||||||
|
constructor(
|
||||||
|
public id: string,
|
||||||
|
public email: string,
|
||||||
|
public verifiedEmail: boolean,
|
||||||
|
public name: string,
|
||||||
|
public given_name: string,
|
||||||
|
public family_name: string,
|
||||||
|
public picture: string,
|
||||||
|
public locale: string,
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
|
|||||||
|
|
||||||
import CreateLineRequest from '@nirvana/core/requests/createLine.request';
|
import CreateLineRequest from '@nirvana/core/requests/createLine.request';
|
||||||
import GetUserLinesResponse from '@nirvana/core/responses/getUserLines.response';
|
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 LoginResponse from '@nirvana/core/responses/login.response';
|
||||||
import MasterLineData from '@nirvana/core/models/masterLineData.model';
|
|
||||||
import NirvanaResponse from '@nirvana/core/responses/nirvanaResponse';
|
import NirvanaResponse from '@nirvana/core/responses/nirvanaResponse';
|
||||||
import UpdateLineMemberState from '@nirvana/core/requests/updateLineMemberState.request';
|
import UpdateLineMemberState from '@nirvana/core/requests/updateLineMemberState.request';
|
||||||
import { User } from '@nirvana/core/models';
|
|
||||||
import UserDetailsResponse from '@nirvana/core/responses/userDetails.response';
|
import UserDetailsResponse from '@nirvana/core/responses/userDetails.response';
|
||||||
import UserSearchResponse from '@nirvana/core/responses/userSearch.response';
|
import UserSearchResponse from '@nirvana/core/responses/userSearch.response';
|
||||||
|
|
||||||
@@ -94,11 +91,3 @@ export async function updateLineMemberState(
|
|||||||
request,
|
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 React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { STORE_ITEMS } from '../electron/constants';
|
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 toast from 'react-hot-toast';
|
||||||
import { useAsyncFn } from 'react-use';
|
import { useAsyncFn } from 'react-use';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { Socket, io } from 'socket.io-client';
|
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 toast from 'react-hot-toast';
|
||||||
import useAuth from './AuthProvider';
|
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 { useAsyncFn, useAsyncRetry } from 'react-use';
|
||||||
|
|
||||||
import { serverCheck } from '../api/NirvanaApi';
|
import { serverCheck } from '../api/NirvanaApi';
|
||||||
|
|
||||||
interface IStabilityContext {
|
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
|
||||||
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import Channels from '../electron/constants';
|
||||||
import { FcGoogle } from 'react-icons/fc';
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
// import Logo from '../../components/Logo';
|
// import Logo from '../../components/Logo';
|
||||||
import { login } from '../../../api/NirvanaApi';
|
import { login } from '../api/NirvanaApi';
|
||||||
import { useAsyncFn } from 'react-use';
|
import { useAsyncFn } from 'react-use';
|
||||||
import Channels from '../../../electron/constants';
|
import useAuth from '../providers/AuthProvider';
|
||||||
import useAuth from '../../../providers/AuthProvider';
|
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { setJwtToken } = useAuth();
|
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 { AuthProvider } from '../providers/AuthProvider';
|
||||||
import { ElectronProvider } from '../providers/ElectronProvider';
|
import { ElectronProvider } from '../providers/ElectronProvider';
|
||||||
|
import ProtectedRoute from './ProtectedRoute';
|
||||||
|
import React from 'react';
|
||||||
|
import { RoomsProvider } from '../providers/RoomsProvider';
|
||||||
import { SocketProvider } from '../providers/SocketProvider';
|
import { SocketProvider } from '../providers/SocketProvider';
|
||||||
import ProtectedRoute from './protected/ProtectedRoute';
|
|
||||||
import { StabilityProvider } from '../providers/StabilityProvider';
|
import { StabilityProvider } from '../providers/StabilityProvider';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
export default function ElectronApp() {
|
export default function ElectronApp() {
|
||||||
return (
|
return (
|
||||||
<StabilityProvider>
|
<StabilityProvider>
|
||||||
@@ -15,9 +14,7 @@ export default function ElectronApp() {
|
|||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<RoomsProvider>
|
<RoomsProvider>
|
||||||
<TerminalProvider>
|
<></>
|
||||||
<></>
|
|
||||||
</TerminalProvider>
|
|
||||||
</RoomsProvider>
|
</RoomsProvider>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</ProtectedRoute>
|
</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