diff --git a/packages/api/index.ts b/packages/api/index.ts index 1ed2a38..4598cc1 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -4,7 +4,6 @@ import { NextFunction } from "express"; import SocketChannels from "@nirvana/core/sockets/channels"; import { UserService } from "./services/user.service"; import { UserStatus } from "@nirvana/core/models"; -import { connectToDatabase } from "./services/database.service"; import cors from "cors"; import getConversationRoutes from "./routes/conversation"; import getSearchRoutes from "./routes/search"; @@ -29,7 +28,6 @@ app.use("/api/conversations", getConversationRoutes()); const PORT = 5000; var server = app.listen(PORT, () => console.log("express running")); -connectToDatabase(); // socket IO stuff diff --git a/packages/api/routes/conversation.ts b/packages/api/routes/conversation.ts index 267615b..398eb63 100644 --- a/packages/api/routes/conversation.ts +++ b/packages/api/routes/conversation.ts @@ -1,11 +1,20 @@ +import { + Conversation, + ConversationMember, + ConversationMemberState, +} from "../../core/models/conversation.model"; +import { JwtClaims, authCheck } from "../middleware/auth"; import express, { Application, Request, Response } from "express"; import Content from "@nirvana/core/models/content.model"; +import { ConversationService } from "../services/conversation.service"; +import CreateConvoRequest from "../../core/requests/createConvo.request"; import GetConversationDetailsResponse from "@nirvana/core/responses/getConversationDetails.response"; +import GetDmConversationByOtherUserIdResponse from "../../core/responses/getDmConversationByOtherUserId.response"; +import MasterConversation from "../../core/models/masterConversation.model"; import { ObjectId } from "mongodb"; import Relationship from "@nirvana/core/models/relationship.model"; import { UserService } from "../services/user.service"; -import { authCheck } from "../middleware/auth"; import { collections } from "../services/database.service"; export default function getConversationRoutes() { @@ -16,5 +25,76 @@ export default function getConversationRoutes() { // get data for a one on one conversation // router.get("/:otherUserGoogleUserId", authCheck, getConversationDetails); + // create a convo + router.post("/", authCheck, createConversation); + + // 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 convos 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 createConversation(req: Request, res: Response) { + try { + const reqObj: CreateConvoRequest = req.body as CreateConvoRequest; + + if (!reqObj?.otherMemberIds.length) { + res.status(400).json("must provide member Ids"); + return; + } + + const userInfo = res.locals.userInfo as JwtClaims; + + const newConvo = new Conversation(); + const convoMembers: ConversationMember[] = + reqObj.otherMemberIds.map((memId) => { + const newConvoMember = new ConversationMember( + newConvo._id, + new ObjectId(memId), + ConversationMemberState.INVITED + ); + + return newConvoMember; + }) ?? []; + + convoMembers.push( + new ConversationMember( + newConvo._id, + new ObjectId(userInfo.userId), + ConversationMemberState.INBOX + ) + ); + + const transactionResult = await ConversationService.createConversation( + newConvo, + convoMembers + ); + + transactionResult + ? res.status(200).json(newConvo) + : res.status(400).json("unable to create convo"); + } catch (error) { + console.log(error); + res.status(500).json(error); + } +} diff --git a/packages/api/services/conversation.service.ts b/packages/api/services/conversation.service.ts index 01ca2e6..1395b25 100644 --- a/packages/api/services/conversation.service.ts +++ b/packages/api/services/conversation.service.ts @@ -1,11 +1,77 @@ +import { + Conversation, + ConversationMember, +} from "../../core/models/conversation.model"; +import { client, collections } from "./database.service"; + +import { ObjectId } from "mongodb"; + export class ConversationService { - // static async getUserById(userId: string) { - // const query = { googleId: userId }; - // const res = await collections.users?.findOne(query); - // // exists - // if (res?._id) { - // return res as User; - // } - // return null; - // } + static async getConversationByOtherUserId(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 createConversation( + convo: Conversation, + convoMembers: ConversationMember[] + ) { + const session = client.startSession(); + try { + const transactionResults = await session.withTransaction(async () => { + // todo: check if convoMembers userId's actually exist + + const insertConvoRes = await collections.conversations?.insertOne( + convo + ); + if (!insertConvoRes?.insertedId) { + await session.abortTransaction(); + console.error("failed to create convo"); + + return; + } + + const insertConvoMembersRes = + await collections.conversationMembers?.insertMany(convoMembers); + if (!insertConvoMembersRes?.insertedCount) { + await session.abortTransaction(); + console.error("failed to create conversation 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; + } } diff --git a/packages/api/services/database.service.ts b/packages/api/services/database.service.ts index 8ea500c..b9b38fc 100644 --- a/packages/api/services/database.service.ts +++ b/packages/api/services/database.service.ts @@ -6,25 +6,34 @@ import { loadConfig } from "../config"; // Global Variables export const collections: { users?: mongoDB.Collection; + conversations?: mongoDB.Collection; + conversationMembers?: mongoDB.Collection; + audioClips?: mongoDB.Collection; } = {}; // Initialize Connection -export async function connectToDatabase() { - const config = loadConfig(); +const config = loadConfig(); - const client: mongoDB.MongoClient = new mongoDB.MongoClient( - config.MONGO_CONNECTION_STRING - ); +export const client: mongoDB.MongoClient = new mongoDB.MongoClient( + config.MONGO_CONNECTION_STRING +); - await client.connect(); +client.connect(); - const db: mongoDB.Db = client.db(process.env.DB_NAME); +const db: mongoDB.Db = client.db(process.env.DB_NAME); - const usersCollection: mongoDB.Collection = db.collection("users"); +const usersCollection: mongoDB.Collection = db.collection("users"); +const convoCollection: mongoDB.Collection = db.collection("conversations"); +const convoMembersCollection: mongoDB.Collection = db.collection( + "conversationMembers" +); +const audioClipsCollection: mongoDB.Collection = db.collection("audioClips"); - collections.users = usersCollection; +collections.users = usersCollection; +collections.conversations = convoCollection; +collections.conversationMembers = convoMembersCollection; +collections.audioClips = audioClipsCollection; - console.log( - `Successfully connected to database: ${db.databaseName} and collections` - ); -} +console.log( + `Successfully connected to database: ${db.databaseName} and collections` +); diff --git a/packages/core/models/conversation.model.ts b/packages/core/models/conversation.model.ts index b1d0c86..c272cf9 100644 --- a/packages/core/models/conversation.model.ts +++ b/packages/core/models/conversation.model.ts @@ -1,25 +1,26 @@ import { ObjectId } from "mongodb"; export class Conversation { - _id: ObjectId; name?: string; - createdDate: Date; - lastUpdatedDate: Date; - - constructor() {} + constructor( + public _id?: ObjectId, + public createdDate: Date = new Date(), + public lastUpdatedDate: Date = new Date() + ) {} } export class ConversationMember { - _id: ObjectId; + constructor( + // unique constraint + public conversationId: ObjectId, + public userId: ObjectId, - // unique constraint - conversationId: ObjectId; - userId: ObjectId; + public state: ConversationMemberState = ConversationMemberState.INVITED, - state: ConversationMemberState; - - createdDate: Date; + public createdDate: Date = new Date(), + public _id?: ObjectId + ) {} } export enum ConversationMemberState { diff --git a/packages/core/requests/createConvo.request.ts b/packages/core/requests/createConvo.request.ts new file mode 100644 index 0000000..1faf1b7 --- /dev/null +++ b/packages/core/requests/createConvo.request.ts @@ -0,0 +1,3 @@ +export default class CreateConvoRequest { + constructor(public otherMemberIds: string[]) {} +} diff --git a/packages/core/responses/getDmConversationByOtherUserId.response.ts b/packages/core/responses/getDmConversationByOtherUserId.response.ts new file mode 100644 index 0000000..3454257 --- /dev/null +++ b/packages/core/responses/getDmConversationByOtherUserId.response.ts @@ -0,0 +1,5 @@ +import { Conversation } from "../models/conversation.model"; + +export default class GetDmConversationByOtherUserIdResponse { + constructor(public conversation: Conversation) {} +} diff --git a/packages/desktop/package.json b/packages/desktop/package.json index f50271f..ddf80cc 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -105,6 +105,7 @@ "typescript": "~4.5.4" }, "dependencies": { + "@nirvana/core": "*", "@emotion/react": "^11.8.2", "@emotion/styled": "^11.8.1", "@getstation/electron-google-oauth2": "^2.1.0", diff --git a/packages/desktop/src/controller/index.tsx b/packages/desktop/src/controller/index.tsx index f2e3aa6..d344d9a 100644 --- a/packages/desktop/src/controller/index.tsx +++ b/packages/desktop/src/controller/index.tsx @@ -31,3 +31,6 @@ export function useUserSearch(searchQuery: string) { } // =========== MUTATIONS +export function useGetDmByUserId() { + return useMutation(ApiCalls.getDmByUserId); +} diff --git a/packages/desktop/src/controller/nirvanaApi.ts b/packages/desktop/src/controller/nirvanaApi.ts index 2ac263e..e52e677 100644 --- a/packages/desktop/src/controller/nirvanaApi.ts +++ b/packages/desktop/src/controller/nirvanaApi.ts @@ -1,5 +1,6 @@ import axios, { AxiosRequestConfig, AxiosResponse, Method } from "axios"; +import { Conversation } from "../../../core/models/conversation.model"; import LoginResponse from "../../../core/responses/login.response"; import { User } from "@nirvana/core/models"; import UserDetailsResponse from "../../../core/responses/userDetails.response"; @@ -69,9 +70,18 @@ async function userSearch(searchQuery: string): Promise { ); } +async function getDmByUserId(otherUserId: string): Promise { + return await NirvanaApi.fetch( + `/conversations/dm/${otherUserId}`, + "GET", + true + ); +} + export const ApiCalls = { login, authCheck, getUserDetails, userSearch, + getDmByUserId, }; diff --git a/packages/desktop/src/pages/Home/newConvo/index.tsx b/packages/desktop/src/pages/Home/newConvo/index.tsx index 4e52944..48dd18a 100644 --- a/packages/desktop/src/pages/Home/newConvo/index.tsx +++ b/packages/desktop/src/pages/Home/newConvo/index.tsx @@ -8,12 +8,18 @@ import { } from "@mui/material"; import { Check, Search } from "@mui/icons-material"; import { useEffect, useState } from "react"; +import { + useGetDmByUserId, + useGetUserDetails, + useUserSearch, +} from "../../../controller/index"; import { $newConvoPage } from "../../../controller/recoil"; +import { ApiCalls } from "../../../controller/nirvanaApi"; import { User } from "@nirvana/core/models/user.model"; import UserRow from "../../../components/User/basicUserDetailsRow"; +import toast from "react-hot-toast"; import { useSetRecoilState } from "recoil"; -import { useUserSearch } from "../../../controller/index"; export default function NewConvo() { const setNewPageConvo = useSetRecoilState($newConvoPage); @@ -21,6 +27,8 @@ export default function NewConvo() { const [debUserSearchQ, setDebUserSearchQ] = useState(""); const [selectedUsers, setSelectedUsers] = useState([]); + const { data: userDetails } = useGetUserDetails(); + const { data, isLoading: isSearching, @@ -101,13 +109,36 @@ export default function NewConvo() { ); } - const createConvo = () => { + const { mutateAsync } = useGetDmByUserId(); + + const createConvo = async () => { + if (!selectedUsers?.length) { + toast.error("Must select at least one person"); + + return; + } + // consolidate the user Ids into an array // make sure that there are no duplicates - console; + // const userIds = selectedUsers.map((selUser) => selUser._id.toString()); + // userIds.push(userDetails.user._id.toString()); + // IF it's a one on one chat // check backend with one route if there is an existing conversation - console; + if (selectedUsers?.length === 1) { + let existingConvo; + try { + existingConvo = await mutateAsync(selectedUsers[0]._id.toString()); + + console.log("there is an existing convo!"); + console.log(existingConvo); + } catch (error) { + console.error(error); + } finally { + console.log("done"); + } + } + // create a conversation object in db with two members, me and this other person // IF it's a group convo/room/channel, create a channel with these people };