diff --git a/src/components/Frame.tsx b/src/components/Frame.tsx index 1143897..8bfb9a4 100644 --- a/src/components/Frame.tsx +++ b/src/components/Frame.tsx @@ -1,19 +1,20 @@ import { IonCard, IonIcon, IonSegment, IonSegmentButton } from '@ionic/react'; import { chatboxOutline, informationCircleOutline, peopleOutline } from 'ionicons/icons'; import React, { useState } from 'react'; +import About from './About'; import './Frame.css'; import Messages from './Messages'; import OnlineList from './OnlineList'; -import About from './About'; type FrameProps = { ownerId: string; roomId: string; userId: string; userList: Map; + joinTime: number; }; -const Frame: React.FC = ({ ownerId, roomId, userId, userList }) => { +const Frame: React.FC = ({ ownerId, roomId, userId, userList, joinTime }) => { const [pane, setPane] = useState('chat'); return ( @@ -30,8 +31,15 @@ const Frame: React.FC = ({ ownerId, roomId, userId, userList }) => { - - + + ); diff --git a/src/components/Messages.tsx b/src/components/Messages.tsx index 710e4f4..eeee441 100644 --- a/src/components/Messages.tsx +++ b/src/components/Messages.tsx @@ -21,6 +21,7 @@ type MessagesProps = { roomId: string; userId: string; userList: Map; + joinTime: number; }; type Message = { @@ -30,8 +31,7 @@ type Message = { senderId: string; }; -const Messages: React.FC = ({ pane, ownerId, roomId, userId, userList }) => { - const [joinTime] = useState(Date.now()); // Time at mounting of the component +const Messages: React.FC = ({ pane, ownerId, roomId, userId, userList, joinTime }) => { const [chats, setChats] = useState([]); // All processed chat messages const [systemMessages, setSystemMessages] = useState([]); // All processed system messages const [allMessages, setAllMessages] = useState([]); // Combined array of chat and system messages diff --git a/src/components/OnlineList.tsx b/src/components/OnlineList.tsx index 3946fd7..dcaef78 100644 --- a/src/components/OnlineList.tsx +++ b/src/components/OnlineList.tsx @@ -1,34 +1,67 @@ import { + IonCol, IonContent, IonFabButton, + IonIcon, IonInput, IonItem, IonLabel, IonList, IonListHeader, - IonToolbar, - IonIcon, IonRow, - IonCol, IonToast, + IonToolbar, } from '@ionic/react'; -import React, { useState, useRef } from 'react'; -import './OnlineList.css'; -import { clipboardOutline } from 'ionicons/icons'; import copy from 'copy-to-clipboard'; +import { clipboardOutline } from 'ionicons/icons'; +import React, { useEffect, useRef, useState } from 'react'; +import { rtdb } from '../services/firebase'; +import './OnlineList.css'; type OnlineListProps = { pane: string; + roomId: string; + userId: string; userList: Map; }; -const OnlineList: React.FC = ({ pane, userList }) => { +const OnlineList: React.FC = ({ pane, roomId, userId, userList }) => { const inputRef = useRef(null); - const [showToast, setShowToast] = useState(false); + const connectionRef = useRef(null); + const [showConnectionChange, setShowConnectionChange] = useState(false); + const [connectionMessage, setConnectionMessage] = useState('You joined the room'); + const [showCopied, setShowCopied] = useState(false); + + // Listen for connection changes in the room + useEffect(() => { + const roomRef = rtdb.ref('/rooms/' + roomId); + roomRef.on('child_added', (snapshot) => { + if (snapshot.val().name !== undefined) { + setConnectionMessage(snapshot.val().name + ' joined'); + } + }); + + roomRef.on('child_removed', (snapshot) => { + if (snapshot.val().name !== undefined && snapshot.key !== userId) { + setConnectionMessage(snapshot.val().name + ' left'); + } + }); + + return () => { + roomRef.off('child_added'); + roomRef.off('child_removed'); + }; + }, [roomId, userId]); + + // Show toast whenever connection message changes + useEffect(() => { + setShowConnectionChange(true); + connectionRef.current?.present(); + }, [connectionMessage]); const copyLink = () => { copy(window.location.href); - setShowToast(true); + setShowCopied(true); }; return ( @@ -55,11 +88,21 @@ const OnlineList: React.FC = ({ pane, userList }) => { setShowToast(false)} - message="Room link copied" color="primary" duration={2000} + isOpen={showCopied} + onDidDismiss={() => setShowCopied(false)} + position="top" + message="Room link copied" + > + setShowConnectionChange(false)} + position="top" + message={connectionMessage} + ref={connectionRef} > ); diff --git a/src/components/VideoPlayer.tsx b/src/components/VideoPlayer.tsx index b40cb8d..c13eecb 100644 --- a/src/components/VideoPlayer.tsx +++ b/src/components/VideoPlayer.tsx @@ -16,18 +16,17 @@ const VideoPlayer: React.FC = ({ ownerId, userId, roomId }) => const [allowUpdate, setAllowUpdate] = useState(true); // Update database on play (owner only) - const onPlay = async () => { + const onPlay = () => { setPlaying(true); if (ownerId === userId) { const currTime = player.current?.getCurrentTime(); if (currTime !== undefined) { - await db.collection('states').doc(roomId).update({ + db.collection('states').doc(roomId).update({ isPlaying: true, time: currTime, }); - await db - .collection('rooms') + db.collection('rooms') .doc(roomId) .update({ requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: currTime, type: 'play' }), @@ -37,18 +36,17 @@ const VideoPlayer: React.FC = ({ ownerId, userId, roomId }) => }; // Update database on pause (owner only) - const onPause = async () => { + const onPause = () => { setPlaying(false); if (ownerId === userId) { const currTime = player.current?.getCurrentTime(); if (currTime !== undefined) { - await db.collection('states').doc(roomId).update({ + db.collection('states').doc(roomId).update({ isPlaying: false, time: currTime, }); - await db - .collection('rooms') + db.collection('rooms') .doc(roomId) .update({ requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: currTime, type: 'pause' }), diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 47a5823..5e73cd7 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -11,6 +11,25 @@ const Home: React.FC = () => { let history = useHistory(); + // Sign in anonymously before finishing loading page content + useEffect(() => { + const authUnsubscribe = auth.onAuthStateChanged(async (user) => { + if (user) { + setUserId(user.uid); + setLoading(false); + } else { + const credential = await auth.signInAnonymously(); + await db.collection('users').doc(credential.user?.uid).set({ + name: generateAnonName(), + }); + } + }); + + return () => { + authUnsubscribe(); + }; + }, []); + // Populate both Firestore and RealTimeDB before navigating to room const createRoom = async () => { // Firestore preparations @@ -37,25 +56,6 @@ const Home: React.FC = () => { return history.push(path); }; - // Sign in anonymously before finishing loading page content - useEffect(() => { - const authUnsubscribe = auth.onAuthStateChanged(async (user) => { - if (user) { - setUserId(user.uid); - setLoading(false); - } else { - const credential = await auth.signInAnonymously(); - await db.collection('users').doc(credential.user?.uid).set({ - name: generateAnonName(), - }); - } - }); - - return () => { - authUnsubscribe(); - }; - }, []); - return ( diff --git a/src/pages/Room.tsx b/src/pages/Room.tsx index 7b573b4..7ba44f1 100644 --- a/src/pages/Room.tsx +++ b/src/pages/Room.tsx @@ -4,7 +4,7 @@ import { RouteComponentProps, useHistory } from 'react-router'; import Frame from '../components/Frame'; import RoomHeader from '../components/RoomHeader'; import VideoPlayer from '../components/VideoPlayer'; -import { auth, db, decrement, increment, rtdb, arrayUnion } from '../services/firebase'; +import { auth, db, decrement, increment, rtdb } from '../services/firebase'; import { generateAnonName } from '../services/utilities'; import './Room.css'; @@ -16,8 +16,8 @@ const Room: React.FC> = ({ match }) => { const [userId, setUserId] = useState(''); const [ownerId, setOwnerId] = useState('undefined'); const [loading, setLoading] = useState(true); - const [userCount, setUserCount] = useState(0); const [userList, setUserList] = useState>(new Map()); + const [joinTime] = useState(Date.now()); // Time at mounting of the component // Verify that the roomId exists in db useEffect(() => { @@ -77,27 +77,26 @@ const Room: React.FC> = ({ match }) => { // Keep userId in the room as long as a connection from the client exists const username = (await db.collection('users').doc(userId).get()).data()?.name; await roomRef.child(userId).set({ name: username }); - await roomRef.update({ userCount: increment }); - await db - .collection('rooms') - .doc(roomId) - .update({ - requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'join' }), - }); + + roomRef.child(userId).onDisconnect().cancel(); // Clear any disconnect actions in case any are queued + roomRef.child(userId).onDisconnect().remove(); // Remove userId from the room when disconnect happens } }); - roomRef.child('userCount').on('value', (snapshot) => { - setUserCount(snapshot.val()); + // Manage user count + rtdb.ref('.info/connected').on('value', (snapshot) => { + if (snapshot.val() === true) { + roomRef.update({ userCount: increment }); + roomRef.onDisconnect().update({ userCount: decrement }); + } }); - // Re-add room into /available/ if the room was deleted - availableRef.on('value', async (snapshot) => { - if (!snapshot.hasChild(roomId)) { - await availableRef.child(roomId).set({ - name: 'Room Name', - createdAt: new Date().toISOString(), - }); + // Remove room availability when the last person leaves + roomRef.child('userCount').on('value', (snapshot) => { + if (snapshot.val() <= 1) { + availableRef.onDisconnect().remove(); + } else { + availableRef.onDisconnect().cancel(); } }); @@ -119,37 +118,6 @@ const Room: React.FC> = ({ match }) => { } }, [userId, validRoom, roomId]); - // Handle disconnect events - useEffect(() => { - if (!loading && userId !== '' && validRoom) { - const depopulate = async () => { - const refUser = rtdb.ref('/rooms/' + roomId + '/' + userId); - const refRoom = rtdb.ref('/rooms/' + roomId); - const refAvailable = rtdb.ref('/available/' + roomId); - const refChat = rtdb.ref('/chats/' + roomId); - - // Always remove user from room on disconnect - await refRoom.onDisconnect().update({ userCount: decrement }); - await refUser.onDisconnect().remove(); - - // Remove the room if the leaving user is the last in the room - if (userCount <= 1) { - await refRoom.onDisconnect().remove(); - await refAvailable.onDisconnect().remove(); - await refChat.onDisconnect().remove(); - } else { - await refRoom.onDisconnect().cancel(); // Cancel all disconnect actions - await refAvailable.onDisconnect().cancel(); - await refChat.onDisconnect().cancel(); - await refRoom.onDisconnect().update({ userCount: decrement }); // User disconnect still needs to be handled - await refUser.onDisconnect().remove(); - } - }; - - depopulate(); - } - }, [userId, validRoom, roomId, loading, userCount]); - return ( @@ -164,7 +132,7 @@ const Room: React.FC> = ({ match }) => { - +