diff --git a/packages/api/index.ts b/packages/api/index.ts index 51bdc67..841e988 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -1,16 +1,9 @@ import express, { Application, Request, Response } from 'express'; -import GetAllSocketClients from '@nirvana/core/sockets/getAllActiveSocketClients'; import InitializeWs from './services/socket.service'; import { NextFunction } from 'express'; 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 getLineRoutes from './routes/line'; import getSearchRoutes from './routes/search'; import getUserRoutes from './routes/user'; import morgan from 'morgan'; @@ -41,7 +34,7 @@ app.use('/api/status', (req: Request, res: Response) => { app.use('/api/user', getUserRoutes()); app.use('/api/search', getSearchRoutes()); -app.use('/api/lines', getLineRoutes()); +// app.use('/api/conversations', getConversationRoutes()); const PORT = process.env.PORT || 8080; const server = app.listen(PORT, () => diff --git a/packages/api/routes/line.ts b/packages/api/routes/line.ts deleted file mode 100644 index 1236511..0000000 --- a/packages/api/routes/line.ts +++ /dev/null @@ -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(resObj)); - } catch (error) { - res.status(500).json(error); - } -} diff --git a/packages/api/routes/user.ts b/packages/api/routes/user.ts index d592d39..e085208 100644 --- a/packages/api/routes/user.ts +++ b/packages/api/routes/user.ts @@ -1,5 +1,5 @@ -import { GoogleUserInfo, User } from '@nirvana/core/models'; import { JwtClaims, authCheck } from '../middleware/auth'; +import User, { GoogleUserInfo, UserStatus } from '@nirvana/core/models/user.model'; import express, { Application, Request, Response } from 'express'; import LoginResponse from '../../core/responses/login.response'; @@ -8,7 +8,6 @@ import { ObjectID } from 'bson'; import { ObjectId } from 'mongodb'; import UserDetailsResponse from '../../core/responses/userDetails.response'; import { UserService } from '../services/user.service'; -import { UserStatus } from '../../core/models/user.model'; import { collections } from '../services/database.service'; import environmentVariables from '../config/config'; diff --git a/packages/api/services/line.service.ts b/packages/api/services/line.service.ts deleted file mode 100644 index bcfa195..0000000 --- a/packages/api/services/line.service.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/api/services/socket.service.ts b/packages/api/services/socket.service.ts index 6e03fb0..5d738b4 100644 --- a/packages/api/services/socket.service.ts +++ b/packages/api/services/socket.service.ts @@ -18,15 +18,7 @@ import { UserStoppedBroadcastingResponse, } from '@nirvana/core/sockets/channels'; -import GetAllSocketClients from '@nirvana/core/sockets/getAllActiveSocketClients'; 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'; // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/packages/api/services/user.service.ts b/packages/api/services/user.service.ts index 443ba9a..d2fdb43 100644 --- a/packages/api/services/user.service.ts +++ b/packages/api/services/user.service.ts @@ -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 { UserStatus } from "../../core/models/user.model"; -import axios from "axios"; -import { collections } from "./database.service"; +import { ObjectId } from 'mongodb'; +import { UserStatus } from '../../core/models/user.model'; +import axios from 'axios'; +import { collections } from './database.service'; export class UserService { static async getUserById(userId: string) { @@ -62,11 +62,11 @@ export class UserService { // based on index defined in Mongo atlas const query = { $search: { - index: "basic user search", + index: 'basic user search', text: { query: searchQuery, path: { - wildcard: "*", + wildcard: '*', }, }, }, @@ -95,13 +95,9 @@ export class UserService { return null; } - static async getGoogleUserInfoWithAccessToken( - accessToken: string - ): Promise { + static async getGoogleUserInfoWithAccessToken(accessToken: string): Promise { return ( - await axios.get( - `https://www.googleapis.com/oauth2/v1/userinfo?access_token=${accessToken}` - ) + await axios.get(`https://www.googleapis.com/oauth2/v1/userinfo?access_token=${accessToken}`) ).data; } diff --git a/packages/core/models/content.model.ts b/packages/core/models/content.model.ts index 4b75edd..e7e455f 100644 --- a/packages/core/models/content.model.ts +++ b/packages/core/models/content.model.ts @@ -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( - public relationshipId: string, // if it's a one on one, this will be the relationship Id - public sentDate: Date, + public creatorUserId: string, public contentUrl: string, - public contentData: string, + public contentType: ContentType, - public _id?: ObjectId, - public listenedDate?: Date + public blobType: string, + + public createdDate = new Date(), + + public id?: string, ) {} } export enum ContentType { - link = "LINK", - audioClip = "AUDIO_CLIP", - text = "TEXT", + audio = 'audio', + link = 'link', + image = 'image', + code = 'code', +} + +export function isUrlImage(url: string) { + return url.match(/\.(jpeg|jpg|gif|png)$/) != null; } diff --git a/packages/core/models/conversation.model.ts b/packages/core/models/conversation.model.ts index e69de29..54b8ced 100644 --- a/packages/core/models/conversation.model.ts +++ b/packages/core/models/conversation.model.ts @@ -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" +} diff --git a/packages/core/models/googleUserInfo.model.ts b/packages/core/models/googleUserInfo.model.ts deleted file mode 100644 index e39225d..0000000 --- a/packages/core/models/googleUserInfo.model.ts +++ /dev/null @@ -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 - ) {} -} diff --git a/packages/core/models/index.ts b/packages/core/models/index.ts deleted file mode 100644 index 9676b82..0000000 --- a/packages/core/models/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./googleUserInfo.model"; -export * from "./user.model"; diff --git a/packages/core/models/line.model.ts b/packages/core/models/line.model.ts deleted file mode 100644 index e4cc0e6..0000000 --- a/packages/core/models/line.model.ts +++ /dev/null @@ -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 -} diff --git a/packages/core/models/masterLineData.model.ts b/packages/core/models/masterLineData.model.ts deleted file mode 100644 index e657299..0000000 --- a/packages/core/models/masterLineData.model.ts +++ /dev/null @@ -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[] */, - ) {} -} diff --git a/packages/core/models/relationship.model.ts b/packages/core/models/relationship.model.ts deleted file mode 100644 index 5d72844..0000000 --- a/packages/core/models/relationship.model.ts +++ /dev/null @@ -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", -} diff --git a/packages/core/models/user.model.ts b/packages/core/models/user.model.ts index 94aea6c..7c6c379 100644 --- a/packages/core/models/user.model.ts +++ b/packages/core/models/user.model.ts @@ -1,8 +1,8 @@ -import { ObjectId } from "mongodb"; +import { ObjectId } from 'mongodb'; -export class User { +export default class User { 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 name: string, @@ -17,12 +17,25 @@ export class User { // additional properties specific to our users collection public status?: UserStatus, public lastUpdatedDate?: Date, - public _id?: ObjectId + public _id?: ObjectId, ) {} } export enum UserStatus { - ONLINE = "ONLINE", - OFFLINE = "OFFLINE", - FLOW_STATE = "FLOW_STATE", + ONLINE = 'ONLINE', + OFFLINE = 'OFFLINE', + 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, + ) {} } diff --git a/packages/desktop/src/api/NirvanaApi.tsx b/packages/desktop/src/api/NirvanaApi.tsx index b92e04d..6481ed8 100644 --- a/packages/desktop/src/api/NirvanaApi.tsx +++ b/packages/desktop/src/api/NirvanaApi.tsx @@ -2,12 +2,9 @@ import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios'; import CreateLineRequest from '@nirvana/core/requests/createLine.request'; import GetUserLinesResponse from '@nirvana/core/responses/getUserLines.response'; -import { Line } from '@nirvana/core/models/line.model'; import LoginResponse from '@nirvana/core/responses/login.response'; -import MasterLineData from '@nirvana/core/models/masterLineData.model'; import NirvanaResponse from '@nirvana/core/responses/nirvanaResponse'; import UpdateLineMemberState from '@nirvana/core/requests/updateLineMemberState.request'; -import { User } from '@nirvana/core/models'; import UserDetailsResponse from '@nirvana/core/responses/userDetails.response'; import UserSearchResponse from '@nirvana/core/responses/userSearch.response'; @@ -94,11 +91,3 @@ export async function updateLineMemberState( request, ); } - -export async function getDmByUserId(otherUserId: string): Promise { - return await NirvanaApi.fetch(`/lines/dm/${otherUserId}`, 'GET', true); -} - -export async function createLine(request: CreateLineRequest): Promise> { - return await NirvanaApi.fetch(`/lines`, 'POST', true, request); -} diff --git a/packages/desktop/src/providers/AuthProvider.tsx b/packages/desktop/src/providers/AuthProvider.tsx index 6dee58d..03675f0 100644 --- a/packages/desktop/src/providers/AuthProvider.tsx +++ b/packages/desktop/src/providers/AuthProvider.tsx @@ -2,7 +2,7 @@ import NirvanaApi, { getUserDetails } from '../api/NirvanaApi'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { STORE_ITEMS } from '../electron/constants'; -import { User } from '@nirvana/core/models/user.model'; +import User from '@nirvana/core/models/user.model'; import toast from 'react-hot-toast'; import { useAsyncFn } from 'react-use'; diff --git a/packages/desktop/src/providers/SocketProvider.tsx b/packages/desktop/src/providers/SocketProvider.tsx index 75c58a2..645dfad 100644 --- a/packages/desktop/src/providers/SocketProvider.tsx +++ b/packages/desktop/src/providers/SocketProvider.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { Socket, io } from 'socket.io-client'; -import FlowState from '../tree/protected/FlowState'; +import FlowState from '../tree/FlowState'; import toast from 'react-hot-toast'; import useAuth from './AuthProvider'; diff --git a/packages/desktop/src/providers/StabilityProvider.tsx b/packages/desktop/src/providers/StabilityProvider.tsx index cd9fc6a..be3cd9c 100644 --- a/packages/desktop/src/providers/StabilityProvider.tsx +++ b/packages/desktop/src/providers/StabilityProvider.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useContext } from 'react'; - +import React, { useContext, useEffect, useState } from 'react'; import { useAsyncFn, useAsyncRetry } from 'react-use'; + import { serverCheck } from '../api/NirvanaApi'; interface IStabilityContext { diff --git a/packages/desktop/src/providers/StreamProvider.tsx b/packages/desktop/src/providers/StreamProvider.tsx deleted file mode 100644 index a2f315e..0000000 --- a/packages/desktop/src/providers/StreamProvider.tsx +++ /dev/null @@ -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: 'webrtc@live.com', - // }, - // { - // 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({ - peerMap: {}, -}); - -export function StreamProvider({ children }: { children: React.ReactChild }) { - const { roomsMap } = useTerminalProvider(); - const { user } = useAuth(); - - const { $ws } = useSockets(); - - const [peerMap, updatePeerMap] = useImmer({}); - - const [userLocalStream, setUserLocalStream] = useState(); - - 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 ( - - {/* handles stream connections */} - {/* {Object.values(roomsMap).map((line) => { - if (line.tunedInMemberIds?.includes(user._id.toString())) - return ( - currMemberId !== user._id.toString(), - )} - handleGotPeerRemoteStream={handleGotPeerRemoteStream} - setLocalStreamForLine={setLocalStreamForLine} - /> - ); - })} */} - - {children} - - ); -} - -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 <>; -} diff --git a/packages/desktop/src/providers/TerminalProvider.tsx b/packages/desktop/src/providers/TerminalProvider.tsx deleted file mode 100644 index b814903..0000000 --- a/packages/desktop/src/providers/TerminalProvider.tsx +++ /dev/null @@ -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({ - 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 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({}); - - const { desktopMode } = useElectron(); - - const [selectedLineId, setSelectedLineId] = useState(); - - const [moveLineState, moveLine] = useAsyncFn(updateLineMemberState); - const [showNewChannelForm, setShowNewChannelForm] = useState(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 ( - - - <> -
- - - {desktopMode === 'mainApp' && } -
- - {children} - -
-
- ); -} - -export default function useTerminalProvider() { - return useContext(TerminalContext); -} diff --git a/packages/desktop/src/providers/ZenProvider.tsx b/packages/desktop/src/providers/ZenProvider.tsx new file mode 100644 index 0000000..09815b1 --- /dev/null +++ b/packages/desktop/src/providers/ZenProvider.tsx @@ -0,0 +1 @@ +// handle the flow state and other things diff --git a/packages/desktop/src/tree/protected/FlowState.tsx b/packages/desktop/src/tree/FlowState.tsx similarity index 100% rename from packages/desktop/src/tree/protected/FlowState.tsx rename to packages/desktop/src/tree/FlowState.tsx diff --git a/packages/desktop/src/tree/protected/login/Login.tsx b/packages/desktop/src/tree/Login.tsx similarity index 96% rename from packages/desktop/src/tree/protected/login/Login.tsx rename to packages/desktop/src/tree/Login.tsx index ed285e7..aa5dcc7 100644 --- a/packages/desktop/src/tree/protected/login/Login.tsx +++ b/packages/desktop/src/tree/Login.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useState } from 'react'; +import Channels from '../electron/constants'; import { FcGoogle } from 'react-icons/fc'; // import Logo from '../../components/Logo'; -import { login } from '../../../api/NirvanaApi'; +import { login } from '../api/NirvanaApi'; import { useAsyncFn } from 'react-use'; -import Channels from '../../../electron/constants'; -import useAuth from '../../../providers/AuthProvider'; +import useAuth from '../providers/AuthProvider'; export default function Login() { const { setJwtToken } = useAuth(); diff --git a/packages/desktop/src/tree/protected/ProtectedRoute.tsx b/packages/desktop/src/tree/ProtectedRoute.tsx similarity index 100% rename from packages/desktop/src/tree/protected/ProtectedRoute.tsx rename to packages/desktop/src/tree/ProtectedRoute.tsx diff --git a/packages/desktop/src/tree/electronApp.tsx b/packages/desktop/src/tree/electronApp.tsx index 328dedd..0cf6cd2 100644 --- a/packages/desktop/src/tree/electronApp.tsx +++ b/packages/desktop/src/tree/electronApp.tsx @@ -1,12 +1,11 @@ -import React from 'react'; -import { Toaster } from 'react-hot-toast'; -import { RoomsProvider } from '../providers/RoomsProvider'; -import { TerminalProvider } from '../providers/TerminalProvider'; import { AuthProvider } from '../providers/AuthProvider'; import { ElectronProvider } from '../providers/ElectronProvider'; +import ProtectedRoute from './ProtectedRoute'; +import React from 'react'; +import { RoomsProvider } from '../providers/RoomsProvider'; import { SocketProvider } from '../providers/SocketProvider'; -import ProtectedRoute from './protected/ProtectedRoute'; import { StabilityProvider } from '../providers/StabilityProvider'; +import { Toaster } from 'react-hot-toast'; export default function ElectronApp() { return ( @@ -15,9 +14,7 @@ export default function ElectronApp() { - - <> - + <> diff --git a/packages/desktop/src/tree/protected/terminal/compose/NewChannelForm.tsx b/packages/desktop/src/tree/protected/terminal/compose/NewChannelForm.tsx deleted file mode 100644 index 762f12d..0000000 --- a/packages/desktop/src/tree/protected/terminal/compose/NewChannelForm.tsx +++ /dev/null @@ -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(''); - - const [userSearchRes, fetchUsers] = useAsyncFn(userSearch); - const [selectedUsers, setSelectedUsers] = useState([]); - - const [isSearchingUsers, setSearchingUsers] = useState(false); - - const searchInputRef = useRef(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 ( -
- - - `esc` - - -
- {/* people search */} - People -
- - -
- - {/* dropdown search results */} -
- {(userSearchRes.loading || isSearchingUsers) && } - - {(!userSearchRes.value || userSearchRes.value?.users.length === 0) && - selectedUsers?.length === 0 && ( - {`Can't find someone? Invite them and tell them the secret passcode!`} - )} - {userSearchRes.value?.users.map((searchedUser) => { - return ( -
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" - > - - - {searchedUser.name} - {searchedUser.email} - - - -
- ); - })} -
- - {/* selected people */} - - {`Selected`} - - - {`${selectedUsers.length}/${maxChannelUserCount - 1}`} - - - - {selectedUsers.map((selectedUser) => { - return ( -
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" - > - - - {selectedUser.name} - {selectedUser.email} - - - -
- ); - })} - - - -
- - - - - `enter` - -
-
-
- ); -} diff --git a/packages/desktop/src/tree/protected/terminal/line/LineDetails.tsx b/packages/desktop/src/tree/protected/terminal/line/LineDetails.tsx deleted file mode 100644 index 736e35e..0000000 --- a/packages/desktop/src/tree/protected/terminal/line/LineDetails.tsx +++ /dev/null @@ -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 ( -
- {/* line details */} -
- {/* channel picture */} - {selectedLine.profilePictures && ( - 0 - ? selectedLine.profilePictures.tunedMembers - : selectedLine.profilePictures.allMembersWithoutMe - } - /> - )} - -
- -

- {selectedLine.lineDetails.name || selectedLine.otherUserObjects[0].givenName} -

- - -
-
- - - - - {selectedLine.profilePictures?.tunedMembers?.map((pictureSrc, index) => ( - - ))} - - - | - - - {selectedLine.profilePictures.untunedMembers.map((pictureSrc, index) => ( - - ))} - -
- - {/* main canvas */} -
- {/* line timeline */} - {/* */} - - {/* live line */} -
- - - Right now - - - {Object.keys(peerMap).map((lineId, index) => { - if (lineId !== selectedLine.lineDetails._id.toString()) return <>; - - return peerMap[lineId]?.peerRelations?.map( - (linePeer) => - linePeer?.peerMediaStream && ( - - ), - ); - })} -
-
- - {/* canvas action buttons */} -
- - - - - - - -
-
- ); -} - -function StreamPlayer({ peerStream }: { peerStream: MediaStream }) { - const streamRef = useRef(null); - - useEffect(() => { - if (streamRef?.current) streamRef.current.srcObject = peerStream; - }, [peerStream]); - - return ( - <> - {/*