mirror of https://github.com/shuang854/Turtle
restructured components
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;
|
||||
@ -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;
|
||||
Loading…
Reference in New Issue