diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx deleted file mode 100644 index 27d6a5c..0000000 --- a/src/components/Chat.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { IonButton, IonCard, IonCardContent, IonCol, IonContent, IonGrid, IonRow, IonTextarea } from '@ionic/react'; -import React, { useEffect, useState } from 'react'; -import { currTime, db, timestamp } from '../services/firebase'; -import './Chat.css'; - -type ChatProps = { - roomId: string; - userId: string; -}; - -type Message = { - id: string; - senderId: string; - sender: string; - content: string; -}; - -const Chat: React.FC = ({ roomId, userId }) => { - const [room] = useState(roomId); - const [message, setMessage] = useState(''); // Message to be sent - const [prevMessages, setPrevMessages] = useState([]); // Track previous messages for updating useEffect - const [newMessages, setNewMessages] = useState([]); // Newly retrieved messages - const [chats, setChats] = useState([ - { id: '', senderId: userId, sender: '', content: 'You have joined the room.' }, - ]); // All received messages - const [loading, setLoading] = useState(true); - - // Listen for new messages - useEffect(() => { - const chatUnsubscribe = db - .collection('rooms') - .doc(room) - .collection('messages') - .orderBy('createdAt') - .where('createdAt', '>', currTime) - .onSnapshot(async (querySnapshot) => { - let newMsgs: Message[] = []; - const changes = querySnapshot.docChanges(); - for (const change of changes) { - if (change.type === 'added') { - const data = change.doc.data(); - const user = await db.collection('users').doc(data?.senderId).get(); - newMsgs.push({ - id: change.doc.id, - senderId: data?.senderId, - sender: user.data()?.name, - content: data?.content, - }); - } - } - - if (newMsgs.length !== 0) { - setNewMessages(newMsgs); - } - }); - - setLoading(false); - return () => { - chatUnsubscribe(); - }; - }, [room]); - - // Only update array containing all messages ('chats') when there are new messages - useEffect(() => { - if (prevMessages !== newMessages) { - setPrevMessages(newMessages); - setChats([...chats, ...newMessages]); - } - }, [prevMessages, newMessages, chats]); - - // Always scroll to most recent chat message (bottom) - useEffect(() => { - console.log('scrolling'); - let content = document.querySelector('ion-content'); - - // Set timeout because DOM doesn't update immediately after 'chats' state is updated - setTimeout(() => { - content?.scrollToBottom(200); - }, 100); - }, [chats]); - - // Send message to database - const sendMessage = async () => { - await db.collection('rooms').doc(roomId).collection('messages').add({ - createdAt: timestamp, - senderId: userId, - content: message, - type: 'user', - }); - - // Reset textarea field - setMessage(''); - }; - - return ( - - - - {!loading ? ( - chats.map((chat) => { - return ( - - - {chat.sender !== '' ? {chat.sender}: : <>} - {chat.content} - - - ); - }) - ) : ( - <> - )} - - - - - - - - setMessage(e.detail.value!)} - value={message} - class="textarea" - > - - - - Send - - - - - - - - ); -}; - -export default Chat; diff --git a/src/components/Chatbox.css b/src/components/Chatbox.css new file mode 100644 index 0000000..bf69b7a --- /dev/null +++ b/src/components/Chatbox.css @@ -0,0 +1,38 @@ +ion-textarea { + border: solid 1px #999; +} + +.chat-card { + height: 100%; + width: 100%; + float: right; + margin: 0; +} + +.input-card-row { + position: fixed; + width: 100%; +} + +.input-card-col { + margin: 0; + border-top: solid 1px #999; +} + +.input-card-content { + padding: 0; +} + +.send-msg { + align-items: center; + justify-content: center; + display: flex; +} + +.send-button { + width: 100%; +} + +.text-area { + height: 100%; +} diff --git a/src/components/Chatbox.tsx b/src/components/Chatbox.tsx new file mode 100644 index 0000000..0c637cd --- /dev/null +++ b/src/components/Chatbox.tsx @@ -0,0 +1,55 @@ +import { IonButton, IonCard, IonCardContent, IonCol, IonRow, IonTextarea } from '@ionic/react'; +import React, { useState } from 'react'; +import { db, timestamp } from '../services/firebase'; +import './Chatbox.css'; +import Messages from './Messages'; + +type ChatboxProps = { + roomId: string; + userId: string; +}; + +const Chat: React.FC = ({ roomId, userId }) => { + const [message, setMessage] = useState(''); // Message to be sent + + // Send message to database + const sendMessage = async () => { + await db.collection('rooms').doc(roomId).collection('messages').add({ + createdAt: timestamp, + senderId: userId, + content: message, + type: 'user', + }); + + // Reset textarea field + setMessage(''); + }; + + return ( + + + + + + + + setMessage(e.detail.value!)} + value={message} + class="textarea" + > + + + + Send + + + + + + + + ); +}; + +export default Chat; diff --git a/src/components/Chat.css b/src/components/Messages.css similarity index 50% rename from src/components/Chat.css rename to src/components/Messages.css index 7f9ee0a..b2cfde0 100644 --- a/src/components/Chat.css +++ b/src/components/Messages.css @@ -6,46 +6,21 @@ ion-col { max-width: 70%; } -ion-textarea { - border: solid 1px #999; -} - -.chat-card { - height: 100%; - width: 100%; - float: right; - margin: 0; -} - -.right-align { - justify-content: flex-end; -} - .message-card { overflow-y: auto; height: calc(100% - 125px); padding: 0; } -.input-card-row { - position: fixed; - width: 100%; -} - -.input-card-col { - margin: 0; - border-top: solid 1px #999; -} - -.input-card-content { - padding: 0; -} - .message-grid { margin-right: 0; height: 100%; } +.right-align { + justify-content: flex-end; +} + .my-msg { background: var(--ion-color-primary); color: #fff; @@ -55,17 +30,3 @@ ion-textarea { background: var(--ion-color-secondary); color: #fff; } - -.send-msg { - align-items: center; - justify-content: center; - display: flex; -} - -.send-button { - width: 100%; -} - -.text-area { - height: 100%; -} diff --git a/src/components/Messages.tsx b/src/components/Messages.tsx new file mode 100644 index 0000000..fe1fc01 --- /dev/null +++ b/src/components/Messages.tsx @@ -0,0 +1,97 @@ +import { IonCol, IonContent, IonGrid, IonRow } from '@ionic/react'; +import React, { useEffect, useState } from 'react'; +import { currTime, db } from '../services/firebase'; +import './Messages.css'; + +type MessagesProps = { + roomId: string; + userId: string; +}; + +type Message = { + id: string; + senderId: string; + sender: string; + content: string; +}; + +const Messages: React.FC = ({ roomId, userId }) => { + const [room] = useState(roomId); + const [chats, setChats] = useState([ + { id: '', senderId: userId, sender: '', content: 'You have joined the room.' }, + ]); // All received messages + const [prevMessages, setPrevMessages] = useState([]); // Track previous messages + const [newMessages, setNewMessages] = useState([]); // Newly retrieved messages + + // Only update array containing all messages ('chats') when there are new messages + useEffect(() => { + if (prevMessages !== newMessages) { + setPrevMessages(newMessages); + setChats([...chats, ...newMessages]); + } + }, [prevMessages, newMessages, chats]); + + // Listen for new messages + useEffect(() => { + const chatUnsubscribe = db + .collection('rooms') + .doc(room) + .collection('messages') + .orderBy('createdAt') + .where('createdAt', '>', currTime) + .onSnapshot(async (querySnapshot) => { + let newMsgs: Message[] = []; + const changes = querySnapshot.docChanges(); + for (const change of changes) { + if (change.type === 'added') { + const data = change.doc.data(); + const user = await db.collection('users').doc(data?.senderId).get(); + newMsgs.push({ + id: change.doc.id, + senderId: data?.senderId, + sender: user.data()?.name, + content: data?.content, + }); + } + } + + if (newMsgs.length !== 0) { + setNewMessages(newMsgs); + } + }); + + return () => { + chatUnsubscribe(); + }; + }, [room]); + + // Always scroll to most recent chat message (bottom) + useEffect(() => { + console.log('scrolling'); + let content = document.querySelector('ion-content'); + + // Set timeout because DOM doesn't update immediately after 'chats' state is updated + setTimeout(() => { + content?.scrollToBottom(200); + }, 100); + }, [chats]); + + return ( + + + {chats.map((chat) => { + return ( + + + {chat.sender !== '' ? {chat.sender}: : <>} + {chat.content} + + + ); + })} + + + ); +}; + +export default Messages; diff --git a/src/components/VideoPlayer.tsx b/src/components/VideoPlayer.tsx new file mode 100644 index 0000000..5af13cb --- /dev/null +++ b/src/components/VideoPlayer.tsx @@ -0,0 +1,100 @@ +import React, { useRef, useState, useEffect } from 'react'; +import ReactPlayer from 'react-player'; +import { db, timestamp } from '../services/firebase'; +import { secondsToTimestamp, timestampToSeconds } from '../services/utilities'; + +type VideoPlayerProps = { + ownerId: string; + userId: string; + roomId: string; +}; + +const VideoPlayer: React.FC = ({ ownerId, userId, roomId }) => { + const player = useRef(null); + const [playing, setPlaying] = useState(false); + + // Send video playing message to database when owner plays video + const onPlay = async () => { + if (ownerId === userId) { + const currTime = player?.current?.getCurrentTime(); + if (currTime !== undefined) { + await db + .collection('rooms') + .doc(roomId) + .collection('messages') + .add({ + createdAt: timestamp, + senderId: userId, + content: 'started playing the video from ' + secondsToTimestamp(currTime), + type: 'play', + }); + } + } + }; + + const onPause = async () => { + if (ownerId === userId) { + const currTime = player?.current?.getCurrentTime(); + if (currTime !== undefined) { + await db + .collection('rooms') + .doc(roomId) + .collection('messages') + .add({ + createdAt: timestamp, + senderId: userId, + content: 'paused the video at ' + secondsToTimestamp(currTime), + type: 'pause', + }); + } + } + }; + + // Listen for video interactions + useEffect(() => { + const videoUnsubscribe = db + .collection('rooms') + .doc(roomId) + .collection('messages') + .where('type', 'in', ['play', 'pause']) + .onSnapshot((querySnapshot) => { + const changes = querySnapshot.docChanges(); + const change = changes[changes.length - 1]; + if (change?.type === 'added') { + const data = change.doc.data(); + if (userId !== data.senderId) { + // Match video timestamp of received message + const arr = data.content.split(' '); + const timestamp = arr[arr.length - 1]; + player.current?.seekTo(timestampToSeconds(timestamp)); + + if (data.type === 'play') { + setPlaying(true); + } else { + setPlaying(false); + } + } + } + }); + + return () => { + videoUnsubscribe(); + }; + }, [roomId, userId]); + + return ( + + ); +}; + +export default VideoPlayer; diff --git a/src/pages/Room.tsx b/src/pages/Room.tsx index 79c160c..f8d69eb 100644 --- a/src/pages/Room.tsx +++ b/src/pages/Room.tsx @@ -1,10 +1,10 @@ import { IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonTitle, IonToolbar } from '@ionic/react'; -import React, { useEffect, useState, useRef } from 'react'; -import ReactPlayer from 'react-player'; +import React, { useEffect, useState } from 'react'; import { RouteComponentProps, useHistory } from 'react-router'; -import Chat from '../components/Chat'; -import { auth, db, decrement, increment, rtdb, timestamp } from '../services/firebase'; -import { generateAnonName, secondsToTimestamp, timestampToSeconds } from '../services/utilities'; +import Chat from '../components/Chatbox'; +import VideoPlayer from '../components/VideoPlayer'; +import { auth, db, decrement, increment, rtdb } from '../services/firebase'; +import { generateAnonName } from '../services/utilities'; import './Room.css'; const Room: React.FC> = ({ match }) => { @@ -16,8 +16,6 @@ const Room: React.FC> = ({ match }) => { const [ownerId, setOwnerId] = useState(''); const [loading, setLoading] = useState(true); const [userCount, setUserCount] = useState(0); - const [playing, setPlaying] = useState(false); - const player = useRef(null); // Verify that the roomId exists in db useEffect(() => { @@ -128,77 +126,6 @@ const Room: React.FC> = ({ match }) => { } }, [userId, validRoom, roomId, loading, userCount]); - // Send video playing message to database when owner plays video - const onPlay = async () => { - if (ownerId === userId) { - const currTime = player?.current?.getCurrentTime(); - if (currTime !== undefined) { - await db - .collection('rooms') - .doc(roomId) - .collection('messages') - .add({ - createdAt: timestamp, - senderId: userId, - content: 'started playing the video from ' + secondsToTimestamp(currTime), - type: 'play', - }); - } - } - }; - - const onPause = async () => { - if (ownerId === userId) { - const currTime = player?.current?.getCurrentTime(); - if (currTime !== undefined) { - await db - .collection('rooms') - .doc(roomId) - .collection('messages') - .add({ - createdAt: timestamp, - senderId: userId, - content: 'paused the video at ' + secondsToTimestamp(currTime), - type: 'pause', - }); - } - } - }; - - // Listen for video interactions - useEffect(() => { - if (!loading) { - const videoUnsubscribe = db - .collection('rooms') - .doc(roomId) - .collection('messages') - .where('type', 'in', ['play', 'pause']) - .onSnapshot((querySnapshot) => { - const changes = querySnapshot.docChanges(); - const change = changes[changes.length - 1]; - if (change?.type === 'added') { - const data = change.doc.data(); - if (userId !== data.senderId) { - // Match video timestamp of received message - const arr = data.content.split(' '); - const timestamp = arr[arr.length - 1]; - player.current?.seekTo(timestampToSeconds(timestamp)); - - if (data.type === 'play') { - setPlaying(true); - } else { - setPlaying(false); - } - } - } - }); - - return () => { - videoUnsubscribe(); - }; - } - }, [loading, roomId, userId]); - return ( @@ -212,17 +139,7 @@ const Room: React.FC> = ({ match }) => { - +