restructured components

pull/3/head
Simon Huang 6 years ago
parent 87a4370e9c
commit d096b50591

@ -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<ChatProps> = ({ roomId, userId }) => {
const [room] = useState(roomId);
const [message, setMessage] = useState(''); // Message to be sent
const [prevMessages, setPrevMessages] = useState<Message[]>([]); // Track previous messages for updating useEffect
const [newMessages, setNewMessages] = useState<Message[]>([]); // Newly retrieved messages
const [chats, setChats] = useState<Message[]>([
{ 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 (
<IonCard class="chat-card">
<IonContent class="message-card">
<IonGrid class="message-grid">
{!loading ? (
chats.map((chat) => {
return (
<IonRow key={chat.id} class={chat.senderId === userId ? 'right-align' : ''}>
<IonCol size="auto" class={chat.senderId === userId ? 'my-msg' : 'other-msg'}>
{chat.sender !== '' ? <b>{chat.sender}: </b> : <></>}
<span>{chat.content}</span>
</IonCol>
</IonRow>
);
})
) : (
<></>
)}
</IonGrid>
</IonContent>
<IonRow class="input-card-row">
<IonCol size="12" sizeLg="3" class="input-card-col">
<IonCardContent class="input-card-content">
<IonRow>
<IonCol size="9">
<IonTextarea
onIonChange={(e) => setMessage(e.detail.value!)}
value={message}
class="textarea"
></IonTextarea>
</IonCol>
<IonCol size="3" class="send-msg">
<IonButton expand="block" color="primary" onClick={sendMessage} class="send-button">
Send
</IonButton>
</IonCol>
</IonRow>
</IonCardContent>
</IonCol>
</IonRow>
</IonCard>
);
};
export default Chat;

@ -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%;
}

@ -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<ChatboxProps> = ({ 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 (
<IonCard class="chat-card">
<Messages roomId={roomId} userId={userId}></Messages>
<IonRow class="input-card-row">
<IonCol size="12" sizeLg="3" class="input-card-col">
<IonCardContent class="input-card-content">
<IonRow>
<IonCol size="9">
<IonTextarea
onIonChange={(e) => setMessage(e.detail.value!)}
value={message}
class="textarea"
></IonTextarea>
</IonCol>
<IonCol size="3" class="send-msg">
<IonButton expand="block" color="primary" onClick={sendMessage} class="send-button">
Send
</IonButton>
</IonCol>
</IonRow>
</IonCardContent>
</IonCol>
</IonRow>
</IonCard>
);
};
export default Chat;

@ -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%;
}

@ -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<MessagesProps> = ({ roomId, userId }) => {
const [room] = useState(roomId);
const [chats, setChats] = useState<Message[]>([
{ id: '', senderId: userId, sender: '', content: 'You have joined the room.' },
]); // All received messages
const [prevMessages, setPrevMessages] = useState<Message[]>([]); // Track previous messages
const [newMessages, setNewMessages] = useState<Message[]>([]); // 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 (
<IonContent class="message-card">
<IonGrid class="message-grid">
{chats.map((chat) => {
return (
<IonRow key={chat.id} class={chat.senderId === userId ? 'right-align' : ''}>
<IonCol size="auto" class={chat.senderId === userId ? 'my-msg' : 'other-msg'}>
{chat.sender !== '' ? <b>{chat.sender}: </b> : <></>}
<span>{chat.content}</span>
</IonCol>
</IonRow>
);
})}
</IonGrid>
</IonContent>
);
};
export default Messages;

@ -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<VideoPlayerProps> = ({ ownerId, userId, roomId }) => {
const player = useRef<ReactPlayer>(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 (
<ReactPlayer
ref={player}
url="https://www.youtube.com/watch?v=ysz5S6PUM-U"
width="100%"
height="100%"
controls={true}
onPlay={onPlay}
onPause={onPause}
playing={playing}
muted={true}
></ReactPlayer>
);
};
export default VideoPlayer;

@ -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<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
@ -16,8 +16,6 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
const [ownerId, setOwnerId] = useState('');
const [loading, setLoading] = useState(true);
const [userCount, setUserCount] = useState(0);
const [playing, setPlaying] = useState(false);
const player = useRef<ReactPlayer>(null);
// Verify that the roomId exists in db
useEffect(() => {
@ -128,77 +126,6 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ 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 (
<IonPage>
<IonHeader>
@ -212,17 +139,7 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
<IonGrid class="room-grid">
<IonRow class="room-row">
<IonCol size="12" sizeLg="9" class="player-col">
<ReactPlayer
ref={player}
url="https://www.youtube.com/watch?v=ysz5S6PUM-U"
width="100%"
height="100%"
controls={true}
onPlay={onPlay}
onPause={onPause}
playing={playing}
muted={true}
></ReactPlayer>
<VideoPlayer ownerId={ownerId} userId={userId} roomId={roomId}></VideoPlayer>
</IonCol>
<IonCol size="12" sizeLg="3" class="chat-col">
<Chat roomId={roomId} userId={userId}></Chat>

Loading…
Cancel
Save