From 1bc7957824a9fd0c3d036cdcd5af3aa6023b02b2 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Mon, 7 Feb 2022 21:57:17 -0800 Subject: [PATCH] joining and leaving backend side done and now agora test here we go --- packages/common/services/agoraService.ts | 6 +- .../common/services/conversationService.ts | 30 +++- packages/common/services/index.ts | 3 + .../Conversations/ConversationFullRow.tsx | 10 +- .../web/components/Conversations/LiveRoom.tsx | 40 ++++- .../components/LiveRooms/LiveRoomsHandler.tsx | 3 + .../MainTabsOrPages/Conversations.tsx | 165 +++++++++++++++++- packages/web/recoil/main.tsx | 4 + 8 files changed, 235 insertions(+), 26 deletions(-) create mode 100644 packages/web/components/LiveRooms/LiveRoomsHandler.tsx diff --git a/packages/common/services/agoraService.ts b/packages/common/services/agoraService.ts index f0f5a96..d3cabcf 100644 --- a/packages/common/services/agoraService.ts +++ b/packages/common/services/agoraService.ts @@ -13,7 +13,7 @@ import AgoraRTC, { import { getFunctions, httpsCallable } from "firebase/functions"; import toast from "react-hot-toast"; -const config: ClientConfig = { +export const config: ClientConfig = { mode: "rtc", codec: "vp8", }; @@ -22,7 +22,7 @@ const config: ClientConfig = { // const useMicrophoneTracks = createMicrophoneAudioTrack(); // TODO: move this to env file -const appId: string = "c8dfd65deb5c4741bd564085627139d0"; //ENTER APP ID HERE +export const appId: string = "c8dfd65deb5c4741bd564085627139d0"; //ENTER APP ID HERE export default class AgoraService { private functions = getFunctions(); @@ -90,5 +90,3 @@ export default class AgoraService { // } // } } - -export { appId }; diff --git a/packages/common/services/conversationService.ts b/packages/common/services/conversationService.ts index 20e08d6..dbf81e4 100644 --- a/packages/common/services/conversationService.ts +++ b/packages/common/services/conversationService.ts @@ -8,6 +8,8 @@ import { setDoc, runTransaction, writeBatch, + arrayUnion, + arrayRemove, } from "firebase/firestore"; import Conversation, { AudioClip, @@ -179,7 +181,33 @@ export default class ConversationService { }, { merge: true } ); + } - return false; + async joinLiveConversation(convoId: string, userJoiningId: string) { + // todo: make sure that user is part of the convo or valid in it + const docRef = doc(this.db, Collections.conversations, convoId); + await setDoc( + docRef, + { + membersInLiveRoom: arrayUnion(userJoiningId), + lastActivityDate: serverTimestamp(), + lastUpdatedBy: userJoiningId, + }, + { merge: true } + ); + } + + async leaveLiveConversation(convoId: string, userJoiningId: string) { + // todo: make sure that user is part of the convo or valid in it + const docRef = doc(this.db, Collections.conversations, convoId); + await setDoc( + docRef, + { + membersInLiveRoom: arrayRemove(userJoiningId), + lastActivityDate: serverTimestamp(), + lastUpdatedBy: userJoiningId, + }, + { merge: true } + ); } } diff --git a/packages/common/services/index.ts b/packages/common/services/index.ts index 9f0bcf8..bf49996 100644 --- a/packages/common/services/index.ts +++ b/packages/common/services/index.ts @@ -1,3 +1,4 @@ +import AgoraService from "./agoraService"; import CloudStorageService from "./cloudStorageService"; import ConversationService from "./conversationService"; import UserService from "./userService"; @@ -7,3 +8,5 @@ export const conversationService = new ConversationService(); export const userService = new UserService(); export const cloudStorageService = new CloudStorageService(); + +export const agoraService = new AgoraService(); diff --git a/packages/web/components/Conversations/ConversationFullRow.tsx b/packages/web/components/Conversations/ConversationFullRow.tsx index 0f18873..1125649 100644 --- a/packages/web/components/Conversations/ConversationFullRow.tsx +++ b/packages/web/components/Conversations/ConversationFullRow.tsx @@ -28,6 +28,8 @@ import { Routes } from "@nirvana/common/helpers/routes"; export default function ConversationFullRow(props: { conversation: Conversation; + handleJoinLive: (conversationId: string) => Promise; + handleLeaveLive: (conversationId: string) => Promise; }) { const { currUser } = useAuth(); const usersConvosMap = useRecoilValue(allUsersConversationsAtom); @@ -75,10 +77,6 @@ export default function ConversationFullRow(props: { audio.play(); }; - const joinLive = () => { - toast("connecting"); - }; - /** * show actions based on which state it is currently in for this user */ @@ -134,7 +132,7 @@ export default function ConversationFullRow(props: { { e.stopPropagation(); - joinLive(); + props.handleJoinLive(props.conversation.id); }} className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200" > @@ -164,7 +162,7 @@ export default function ConversationFullRow(props: { isNewIncoming ? "text-slate-400 font-semibold" : "text-slate-300" }`} > - {moment(props.conversation.lastActivityDate.toDate()).fromNow()} + {moment(props.conversation.lastActivityDate?.toDate()).fromNow()} {/* actions */} diff --git a/packages/web/components/Conversations/LiveRoom.tsx b/packages/web/components/Conversations/LiveRoom.tsx index 8622844..a81675c 100644 --- a/packages/web/components/Conversations/LiveRoom.tsx +++ b/packages/web/components/Conversations/LiveRoom.tsx @@ -1,9 +1,18 @@ import Conversation from "@nirvana/common/models/conversation"; +import { agoraService, conversationService } from "@nirvana/common/services"; import { Avatar, Tooltip } from "antd"; -import { FaWalking } from "react-icons/fa"; +import toast from "react-hot-toast"; +import { FaPhoneSlash, FaWalking } from "react-icons/fa"; +import { useAuth } from "../../contexts/authContext"; import { MasterAvatarGroupWithUserFetch } from "../UserDetails/MasterAvatarGroup"; -export default function LiveRoom(props: { conversation: Conversation }) { +export default function LiveRoom(props: { + conversation: Conversation; + handleJoinLive: (conversationId: string) => Promise; + handleLeaveLive: (conversationId: string) => Promise; +}) { + const { currUser } = useAuth(); + return ( - - + props.handleLeaveLive(props.conversation.id)} + className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200 + absolute right-5 text-slate-50 group-hover:text-red-600 text-lg transition-all -z-10 group-hover:z-10" + > + + + + ) : ( + + props.handleJoinLive(props.conversation.id)} + className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200 absolute right-5 text-slate-50 group-hover:text-teal-600 text-lg transition-all -z-10 group-hover:z-10" - > - - - + > + + + + )} ); } diff --git a/packages/web/components/LiveRooms/LiveRoomsHandler.tsx b/packages/web/components/LiveRooms/LiveRoomsHandler.tsx new file mode 100644 index 0000000..65479ea --- /dev/null +++ b/packages/web/components/LiveRooms/LiveRoomsHandler.tsx @@ -0,0 +1,3 @@ +export default function LiveRoomsHandler() { + return <>; +} diff --git a/packages/web/components/MainTabsOrPages/Conversations.tsx b/packages/web/components/MainTabsOrPages/Conversations.tsx index 980e2a0..e006f6d 100644 --- a/packages/web/components/MainTabsOrPages/Conversations.tsx +++ b/packages/web/components/MainTabsOrPages/Conversations.tsx @@ -24,11 +24,139 @@ import { RelativeTimeSeparatedConvosSelector, } from "../../recoil/main"; import ConversationFullRow from "../Conversations/ConversationFullRow"; +import LiveRooms from "../LiveRooms/LiveRoomsHandler"; +import LiveRoomsHandler from "../LiveRooms/LiveRoomsHandler"; +import { + ClientConfig, + IAgoraRTC, + IAgoraRTCClient, + IMicrophoneAudioTrack, +} from "agora-rtc-sdk-ng"; +import { useEffect, useState } from "react"; +import { config, appId } from "@nirvana/common/services/agoraService"; +import toast from "react-hot-toast"; +import { agoraService, conversationService } from "@nirvana/common/services"; +import { useAuth } from "../../contexts/authContext"; export default function Conversations() { const router = useRouter(); + const { currUser } = useAuth(); + const liveRooms = useRecoilValue(liveRoomsSelector); + + const [agoraRtc, setAgoraRtc] = useState(); + const [agoraRtcClient, setAgoraRtcClient] = useState(); + const [localAudioTrack, setLocalAudioTrack] = + useState(); + + // set up agora stuff + useEffect(() => { + (async function () { + // dynamic import as the server side import doesn't work + const AgoraRTC = (await import("agora-rtc-sdk-ng")).default; + const agoraClient = AgoraRTC.createClient(config); + + setAgoraRtcClient(agoraClient); + setAgoraRtc(AgoraRTC); + })(); + }, []); + + async function handleJoinChannel(channelName: string, agoraToken: string) { + const localTrack: IMicrophoneAudioTrack = + await agoraRtc!.createMicrophoneAudioTrack(); + + setLocalAudioTrack(localTrack); + + const init = async (chanName: string) => { + agoraRtcClient!.on("user-published", async (user, mediaType) => { + await agoraRtcClient!.subscribe(user, mediaType); + console.log("subscribe success"); + + if (mediaType === "audio") { + user.audioTrack?.play(); + } + }); + + agoraRtcClient!.on("user-unpublished", async (user, type) => { + console.log("unpublished", user, type); + if (type === "audio") { + user.audioTrack?.stop(); + } + + await agoraRtcClient!.unsubscribe(user); + }); + + agoraRtcClient!.on("user-left", (user) => { + console.log("user left", user); + }); + + await agoraRtcClient!.join(appId, chanName, agoraToken, null); + if (localTrack) await agoraRtcClient!.publish(localTrack); + }; + + if (localTrack) { + console.log("init ready"); + init(channelName); + } else { + toast.error("Not ready for joining call"); + return; + } + } + + async function handleLeaveChannel() { + // destroy local track + localAudioTrack?.close(); + + // leave all channels + await agoraRtcClient!.leave(); + } + + // click on join + // add me to the active list of people in the call + // update the conversation lastActivityDate as well + + const handleJoinLive = async (conversationId: string) => { + const connectingToast = toast.loading("connecting"); + + try { + // agora token from CF + const agoraToken = await agoraService.getAgoraToken(conversationId); + + console.log(agoraToken); + + await conversationService.joinLiveConversation( + conversationId, + currUser!.uid + ); + } catch { + toast.error("problem in joining"); + } + + toast.remove(connectingToast); + }; + + const handleLeaveLive = async (conversationId: string) => { + const leavingToast = toast.loading("leaving"); + + try { + await conversationService.leaveLiveConversation( + conversationId, + currUser!.uid + ); + } catch { + toast.error("problem in joining"); + } + + toast.remove(leavingToast); + }; + + // agora start the call stream + + // leave the call on before unload if in a call + + // update the database to remove my userId from the list of people in the room + const relativeConvoSections = useRecoilValue( RelativeTimeSeparatedConvosSelector ); @@ -68,7 +196,7 @@ export default function Conversations() { return ( <> - {/* live */} + {/* live rooms */} {liveRooms?.length > 0 && ( <> @@ -81,7 +209,12 @@ export default function Conversations() { {/* row of live room cards */} {liveRooms.map((lRoom) => ( - + ))} @@ -97,7 +230,12 @@ export default function Conversations() { {todayConvos.map((tRoom) => ( - + ))} @@ -113,7 +251,12 @@ export default function Conversations() { {last7DaysConvos.map((tRoom) => ( - + ))} @@ -129,7 +272,12 @@ export default function Conversations() { {earlierMonthConvos.map((tRoom) => ( - + ))} @@ -145,7 +293,12 @@ export default function Conversations() { {oldJunkConvos.map((tRoom) => ( - + ))} diff --git a/packages/web/recoil/main.tsx b/packages/web/recoil/main.tsx index e872a26..1555d45 100644 --- a/packages/web/recoil/main.tsx +++ b/packages/web/recoil/main.tsx @@ -384,6 +384,10 @@ export const checkIncomingMessageSelector = selectorFamily({ return true; } + if (!currConvo?.lastActivityDate) { + return false; + } + if ( currConvo.lastActivityDate.toDate() > currUserConvoAssoc.lastInteractionDate?.toDate()