From e5ccfaed107fad30fd18c5347712654cb46ab836 Mon Sep 17 00:00:00 2001 From: talksik Date: Mon, 21 Mar 2022 22:50:52 -0400 Subject: [PATCH] getting some socket stuff set up on client and server --- packages/api/index.ts | 63 ++++++++++++++++--- .../core/responses/getContacts.response.ts | 2 + packages/core/sockets/channels.ts | 9 +++ packages/desktop/package.json | 9 +-- packages/desktop/src/controller/index.tsx | 56 ++++++++++++++++- packages/desktop/src/index.ts | 2 +- packages/desktop/src/nirvanaApp.tsx | 3 + .../src/pages/Home/conversations/index.tsx | 59 ++++++++++------- .../pages/Home/selectedConversation/index.tsx | 9 +++ packages/desktop/src/pages/Login/index.tsx | 2 + 10 files changed, 175 insertions(+), 39 deletions(-) create mode 100644 packages/core/sockets/channels.ts diff --git a/packages/api/index.ts b/packages/api/index.ts index 85ce000..226c010 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -1,6 +1,7 @@ import express, { Application, Request, Response } from "express"; import { NextFunction } from "express"; +import SocketChannels from "@nirvana/core/sockets/channels"; import { connectToDatabase } from "./services/database.service"; import cors from "cors"; import getContactsRoutes from "./routes/contacts"; @@ -27,15 +28,61 @@ app.use("/api/search", getSearchRoutes()); app.use("/api/conversations", getConversationRoutes()); app.use("/api/contacts", getContactsRoutes()); -const server = new http.Server(app); -server.listen(5000, () => - console.log("Example app is listening on port 5000.") -); +const PORT = 5000; +var server = app.listen(PORT, () => console.log("express running")); +connectToDatabase(); -const io = require("socket.io")(server); +// socket IO stuff -io.on("connection", function (socket: any) { - console.log("a user connected"); +const io = require("socket.io")(server, { + // todo: add protection + cors: { + origin: "*", + }, }); -connectToDatabase(); +io.on("connection", function (socket: any) { + // add to map between between googleUserIds and socketId + + console.log("a user connected"); + + // ===== JOIN ==== + /** User wants to subscribe to live emissions of a conversation */ + socket.on(SocketChannels.JOIN_ROOM, (relationshipId: string) => { + // add this user to the room + + console.log( + `${socket.id} user joined room for relationship ${relationshipId}` + ); + + socket.join(relationshipId); + }); + + // ==== UPDATES ==== + // can be a change of: + // 1. status...later...do short polling for this instead + // 2. new content: audio clip or link + // 3. someone is starting to speak + // send message to everyone in room except sender... + + /** User wants to send some update to a particular room */ + socket.on( + SocketChannels.SEND_AUDIO_CLIP, + (relationshipId: string, data: any) => {} + ); + + socket.on( + SocketChannels.SEND_STARTED_SPEAKING, + (relationshipId: string, data: any) => {} + ); + + socket.on( + SocketChannels.SEND_STOPPED_SPEAKING, + (relationshipId: string, data: any) => {} + ); + + // ==== DISCONNECT ==== + socket.on("disconnect", () => { + console.log("user disconnected"); + }); +}); diff --git a/packages/core/responses/getContacts.response.ts b/packages/core/responses/getContacts.response.ts index bc29daf..d9b2523 100644 --- a/packages/core/responses/getContacts.response.ts +++ b/packages/core/responses/getContacts.response.ts @@ -8,5 +8,7 @@ export default class GetContactsResponse { } export class ContactDetails { + isSpeaking: boolean = false; + constructor(public otherUser: User, public relationship: Relationship) {} } diff --git a/packages/core/sockets/channels.ts b/packages/core/sockets/channels.ts new file mode 100644 index 0000000..cf78af7 --- /dev/null +++ b/packages/core/sockets/channels.ts @@ -0,0 +1,9 @@ +enum SocketChannels { + JOIN_ROOM = "JOIN_ROOM", + SEND_AUDIO_CLIP = "SEND_AUDIO_CLIP", + SEND_USER_STATUS_UPDATE = "SEND_USER_STATUS_UPDATE", + SEND_STARTED_SPEAKING = "SEND_STARTED_SPEAKING", + SEND_STOPPED_SPEAKING = "SEND_STOPPED_SPEAKING", +} + +export default SocketChannels; diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 29ebdb9..4d30e16 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -46,10 +46,10 @@ [ "@electron-forge/plugin-webpack", { - "port": "3001", - "loggerPort": "9001", + "port": "3000", + "loggerPort": "9000", "mainConfig": "./webpack.main.config.js", - "devContentSecurityPolicy": "connect-src 'self' http://localhost:5000 'unsafe-eval'", + "devContentSecurityPolicy": "connect-src 'self' http://localhost:5000 ws://localhost:5000 'unsafe-eval'", "renderer": { "config": "./webpack.renderer.config.js", "entryPoints": [ @@ -112,6 +112,7 @@ "react-hotkeys": "^2.0.0", "react-icons": "^4.3.1", "react-query": "^3.34.16", - "recoil": "^0.6.1" + "recoil": "^0.6.1", + "socket.io-client": "^4.4.1" } } diff --git a/packages/desktop/src/controller/index.tsx b/packages/desktop/src/controller/index.tsx index 5b25b7e..34382f2 100644 --- a/packages/desktop/src/controller/index.tsx +++ b/packages/desktop/src/controller/index.tsx @@ -1,5 +1,7 @@ import { $authTokens, $searchQuery } from "./recoil"; import axios, { AxiosResponse } from "axios"; +import { queryClient, socket } from "../nirvanaApp"; +import { useEffect, useState } from "react"; import { useMutation, useQuery } from "react-query"; import GetContactsResponse from "../../../core/responses/getContacts.response"; @@ -7,10 +9,10 @@ import GetConversationDetailsResponse from "@nirvana/core/responses/getConversat import { ObjectId } from "mongodb"; import { RelationshipState } from "@nirvana/core/models/relationship.model"; import SearchResponse from "@nirvana/core/responses/search.response"; +import SocketChannels from "@nirvana/core/sockets/channels"; import UpdateRelationshipStateRequest from "../../../core/requests/updateRelationshipState.request"; import { User } from "@nirvana/core/models"; import { nirvanaApi } from "./nirvanaApi"; -import { queryClient } from "../nirvanaApp"; import { useRecoilValue } from "recoil"; // =========== API @@ -133,13 +135,63 @@ export function useConversationDetails(otherUserGoogleId: string) { export function useGetAllContactBasicDetails() { const authTokens = useRecoilValue($authTokens); - return useQuery( + // relationshipId's of the conversations where there is someone speaking + const [speakingRooms, setSpeakingRooms] = useState([]); + + useEffect(() => { + socket.on( + SocketChannels.SEND_STARTED_SPEAKING, + (relationshipId: string) => { + setSpeakingRooms((prevSpeakingRooms) => [ + ...prevSpeakingRooms, + relationshipId, + ]); + } + ); + + socket.on( + SocketChannels.SEND_STOPPED_SPEAKING, + (relationshipId: string) => { + setSpeakingRooms((prevSpeakingRooms) => + prevSpeakingRooms.filter( + (relationshipRoomId) => relationshipRoomId !== relationshipId + ) + ); + } + ); + + return () => { + socket.removeAllListeners(SocketChannels.SEND_STARTED_SPEAKING); + socket.removeAllListeners(SocketChannels.SEND_STOPPED_SPEAKING); + }; + }, []); + + const reactQueryRes = useQuery( Querytypes.GET_CONTACTS_RELATIONSHIPS, () => getContactsBasicDetails(authTokens.idToken), { refetchOnWindowFocus: false, } ); + + // go through all conversations and mutate adding in data + // on whether or not someone is speaking or not + if (reactQueryRes.data) { + reactQueryRes.data.contactsDetails.map((contactDet) => { + // todo: join the right rooms based on the relevant contacts/conversations returned here + socket.emit( + SocketChannels.JOIN_ROOM, + contactDet.relationship._id.toString() + ); + + // if this contact/conversation is in the list of speaking ones, then change isSpeaking + if (speakingRooms.includes(contactDet.relationship._id.toString())) { + contactDet.isSpeaking = true; + } + }); + } + + return reactQueryRes; } // =========== MUTATIONS diff --git a/packages/desktop/src/index.ts b/packages/desktop/src/index.ts index e82e4a5..4315515 100644 --- a/packages/desktop/src/index.ts +++ b/packages/desktop/src/index.ts @@ -34,7 +34,7 @@ const createWindow = (): void => { browserWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); // Open the DevTools. - browserWindow.webContents.openDevTools({ mode: "detach" }); + browserWindow.webContents.openDevTools({ mode: "right" }); }; // This method will be called when Electron has finished diff --git a/packages/desktop/src/nirvanaApp.tsx b/packages/desktop/src/nirvanaApp.tsx index 43276c0..88e7411 100644 --- a/packages/desktop/src/nirvanaApp.tsx +++ b/packages/desktop/src/nirvanaApp.tsx @@ -5,6 +5,7 @@ import Login from "./pages/Login"; import ProtectedRoute from "./components/ProtectedRoute"; import { ReactQueryDevtools } from "react-query/devtools"; import { RecoilRoot } from "recoil"; +import io from "socket.io-client"; import testConnection from "@nirvana/core"; testConnection(); @@ -12,6 +13,8 @@ testConnection(); // Create a client export const queryClient = new QueryClient(); +export const socket = io("http://localhost:5000"); + function NirvanaApp() { return ( <> diff --git a/packages/desktop/src/pages/Home/conversations/index.tsx b/packages/desktop/src/pages/Home/conversations/index.tsx index 67068ad..b6494b7 100644 --- a/packages/desktop/src/pages/Home/conversations/index.tsx +++ b/packages/desktop/src/pages/Home/conversations/index.tsx @@ -2,6 +2,7 @@ import { Add, LinkRounded } from "@mui/icons-material"; import { $selectedConversation } from "../../../controller/recoil"; import { Avatar } from "antd"; +import { ContactDetails } from "@nirvana/core/responses/getContacts.response"; import { FaVolumeUp } from "react-icons/fa"; import SkeletonLoader from "../../../components/loading/skeleton"; import UserAvatarWithStatus from "../../../components/User/userAvatarWithStatus"; @@ -55,36 +56,46 @@ export default function Conversations() { - {isLoading ? : null} -
- - - Inbox + {isLoading ? ( + + ) : ( +
+ + + Inbox + - - {contactDetailsListResponse?.contactsDetails.map((contactDetail) => { - return ( -
selectContact(contactDetail.otherUser.googleId)} - className="flex flex-row items-center hover:bg-slate-600 group border-t border-t-slate-500 + {contactDetailsListResponse?.contactsDetails.map( + (contactDetail: ContactDetails) => { + return ( +
+ selectContact(contactDetail.otherUser.googleId) + } + className="flex flex-row items-center hover:bg-slate-600 group border-t border-t-slate-500 py-4 px-2 cursor-pointer" - > - + > + - - {contactDetail.otherUser.name} - + + {contactDetail.otherUser.name} + - - - + + + - speaking... -
- ); - })} -
+ {contactDetail.isSpeaking ? ( + speaking... + ) : null} +
+ ); + } + )} +
+ )} ); } diff --git a/packages/desktop/src/pages/Home/selectedConversation/index.tsx b/packages/desktop/src/pages/Home/selectedConversation/index.tsx index 91ba1d2..57b9666 100644 --- a/packages/desktop/src/pages/Home/selectedConversation/index.tsx +++ b/packages/desktop/src/pages/Home/selectedConversation/index.tsx @@ -11,10 +11,12 @@ import { Dimensions } from "../../../electron/constants"; import { FaWindowClose } from "react-icons/fa"; import { GlobalHotKeys } from "react-hotkeys"; import { RelationshipState } from "@nirvana/core/models/relationship.model"; +import SocketChannels from "@nirvana/core/sockets/channels"; import UpdateRelationshipStateRequest from "@nirvana/core/requests/updateRelationshipState.request"; import UserStatusText from "../../../components/User/userStatusText"; import moment from "moment"; import { queryClient } from "../../../nirvanaApp"; +import { socket } from "../../../nirvanaApp"; import { useEffect } from "react"; import { useRecoilState } from "recoil"; @@ -126,6 +128,13 @@ export default function SelectedConversation() { } }; + // emit to room/conversation that someone started speaking + const socketStartedSpeaking = () => { + // send the room name and the update type + + socket.emit(SocketChannels.SEND_STARTED_SPEAKING); + }; + // hot keys for closing this window const handleClose = () => { setSelectedConvo(null); diff --git a/packages/desktop/src/pages/Login/index.tsx b/packages/desktop/src/pages/Login/index.tsx index 2e39de7..e79b3ea 100644 --- a/packages/desktop/src/pages/Login/index.tsx +++ b/packages/desktop/src/pages/Login/index.tsx @@ -45,6 +45,8 @@ export default function Login({ onReady }: { onReady: Function }) { if (authFailureCount >= 2) { logOut(); } else { + // todo: comment all of this else when trying to test with two users/instances + // see if we have tokens in localstorage in which case we can continue on window.electronAPI.store .get(STORE_ITEMS.AUTH_TOKENS)