finished database restructuring

pull/3/head
Simon Huang 6 years ago
parent fb90ca1c77
commit fe1934d7b3

@ -37,6 +37,7 @@ const collection = {
requests: [
{
createdAt: 'timestamp',
time: '01:25:44', // Relevant for 'play', 'pause' types
type: 'updateState',
senderId: 'userId',
},

@ -1,7 +1,7 @@
import { IonCard, IonFabButton, IonFooter, IonIcon, IonInput, IonToolbar } from '@ionic/react';
import { sendOutline } from 'ionicons/icons';
import React, { useState } from 'react';
import { db, timestamp } from '../services/firebase';
import { db, arrayUnion } from '../services/firebase';
import './Chatbox.css';
import Messages from './Messages';
import OnlineList from './OnlineList';
@ -10,7 +10,7 @@ type ChatboxProps = {
ownerId: string;
roomId: string;
userId: string;
userList: string[];
userList: Map<string, string>;
};
const Chat: React.FC<ChatboxProps> = ({ ownerId, roomId, userId, userList }) => {
@ -19,16 +19,20 @@ const Chat: React.FC<ChatboxProps> = ({ ownerId, roomId, userId, userList }) =>
// Send message to database
const sendMessage = async () => {
if (message !== '') {
await db.collection('rooms').doc(roomId).collection('messages').add({
createdAt: timestamp,
senderId: userId,
content: message,
type: 'chat',
});
}
await db
.collection('chats')
.doc(roomId)
.update({
messages: arrayUnion({
createdAt: Date.now(),
senderId: userId,
content: message,
}),
});
// Reset textarea field
setMessage('');
// Reset textarea field
setMessage('');
}
};
const onEnter = (e: React.KeyboardEvent<HTMLIonInputElement>) => {
@ -39,7 +43,7 @@ const Chat: React.FC<ChatboxProps> = ({ ownerId, roomId, userId, userList }) =>
return (
<IonCard class="chat-card">
<Messages ownerId={ownerId} roomId={roomId} userId={userId}></Messages>
<Messages ownerId={ownerId} roomId={roomId} userId={userId} userList={userList}></Messages>
<IonFooter>
<IonToolbar class="message-toolbar">
<IonInput

@ -1,36 +1,41 @@
import { IonCol, IonContent, IonGrid, IonRow } from '@ionic/react';
import React, { useEffect, useRef, useState } from 'react';
import { currTime, db, TimestampType } from '../services/firebase';
import { db, arrayUnion } from '../services/firebase';
import './Messages.css';
import { secondsToTimestamp } from '../services/utilities';
type MessagesProps = {
ownerId: string;
roomId: string;
userId: string;
userList: Map<string, string>;
};
type Message = {
id: string;
senderId: string;
sender: string;
content: string;
};
type RawMessage = {
content: string;
createdAt: TimestampType;
createdAt: number;
id: string;
senderId: string;
};
const Messages: React.FC<MessagesProps> = ({ ownerId, roomId, userId }) => {
const [chats, setChats] = useState<Message[]>([
{ id: '', senderId: userId, sender: '', content: 'You have joined the room.' },
]); // All received messages
const [messages, setMessages] = useState<RawMessage[]>();
const [prevMessages, setPrevMessages] = useState<Message[]>([]); // Track previous messages
const Messages: React.FC<MessagesProps> = ({ ownerId, roomId, userId, userList }) => {
const [joinTime] = useState(Date.now()); // Time at mounting of the component
const [chats, setChats] = useState<Message[]>([]); // All processed chat messages
const [systemMessages, setSystemMessages] = useState<Message[]>([]); // All processed system messages
const [allMessages, setAllMessages] = useState<Message[]>([]); // Combined array of chat and system messages
const [userHistory] = useState<Map<string, string>>(new Map<string, string>()); // All users who are/were in the room
const contentRef = useRef<HTMLIonContentElement>(null);
// Listen for new messages
// Send 'joined room' message on component mount
useEffect(() => {
db.collection('rooms')
.doc(roomId)
.update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'join' }),
});
}, [roomId, userId]);
// Listen for new chat messages
useEffect(() => {
const chatUnsubscribe = db
.collection('chats')
@ -39,18 +44,84 @@ const Messages: React.FC<MessagesProps> = ({ ownerId, roomId, userId }) => {
const data = docSnapshot.data();
if (data !== undefined) {
const messages = data.messages;
setMessages(messages);
let arr: Message[] = [];
for (const msg of messages) {
if (msg.createdAt > joinTime) {
arr.push({
content: msg.content,
createdAt: msg.createdAt,
id: msg.senderId + msg.createdAt,
senderId: msg.senderId,
});
}
}
setChats(arr);
}
});
return () => {
chatUnsubscribe();
};
}, [roomId, ownerId]);
}, [roomId, joinTime]);
// Listen for list of users in the room
// Convert request type to message content
const processType = (type: string, time: number): string => {
switch (type) {
case 'change':
return 'changed the video.';
case 'join':
return 'joined the room.';
case 'pause':
return 'paused the video at ' + secondsToTimestamp(time);
case 'play':
return 'played the video from ' + secondsToTimestamp(time);
default:
return '';
}
};
useEffect(() => {}, [messages]);
// Listen for new system messages
useEffect(() => {
const roomUnsubscribe = db
.collection('rooms')
.doc(roomId)
.onSnapshot((docSnapshot) => {
const docData = docSnapshot.data();
if (docData !== undefined) {
const requests = docData.requests;
let arr: Message[] = [];
for (const req of requests) {
if (req.createdAt > joinTime && req.type !== 'updateState') {
arr.push({
content: processType(req.type, req.time),
createdAt: req.createdAt,
id: req.senderId + req.createdAt,
senderId: req.senderId,
});
}
}
setSystemMessages(arr);
}
});
return () => {
roomUnsubscribe();
};
}, [roomId, joinTime]);
// Maintain list of users who entered the room, in order to keep all sender names of messages in the room
useEffect(() => {
userList.forEach((name: string, id: string) => {
userHistory.set(id, name);
});
}, [userList, userHistory]);
// Combine messages
useEffect(() => {
setAllMessages(chats.concat(systemMessages));
}, [chats, systemMessages]);
// Always scroll to most recent chat message (bottom)
useEffect(() => {
@ -60,21 +131,32 @@ const Messages: React.FC<MessagesProps> = ({ ownerId, roomId, userId }) => {
setTimeout(() => {
content?.scrollToBottom(200);
}, 100);
}, [chats]);
}, [allMessages]);
// Retrieve display name from userId
const getName = (id: string) => {
let name = userHistory.get(id);
if (id === ownerId) {
name += ' 👑';
}
return name;
};
return (
<IonContent class="message-card" ref={contentRef}>
<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>
);
})}
{allMessages
.sort((msg1, msg2) => msg1.createdAt - msg2.createdAt)
.map((msg) => {
return (
<IonRow key={msg.id} class={msg.senderId === userId ? 'right-align' : ''}>
<IonCol size="auto" class={msg.senderId === userId ? 'my-msg' : 'other-msg'}>
{getName(msg.senderId) !== '' ? <b>{getName(msg.senderId)}: </b> : <></>}
<span>{msg.content}</span>
</IonCol>
</IonRow>
);
})}
</IonGrid>
</IonContent>
);

@ -4,7 +4,7 @@ import { peopleOutline } from 'ionicons/icons';
import './OnlineList.css';
type OnlineListProps = {
userList: string[];
userList: Map<string, string>;
};
const OnlineList: React.FC<OnlineListProps> = ({ userList }) => {
@ -24,7 +24,7 @@ const OnlineList: React.FC<OnlineListProps> = ({ userList }) => {
>
<IonList class="popover-list">
<IonListHeader class="list-header">Online</IonListHeader>
{userList.map((user) => {
{Array.from(userList.values()).map((user) => {
return (
<IonItem key={user} class="online-item" lines="none">
<IonLabel class="online-label">{user}</IonLabel>

@ -2,7 +2,7 @@ import { IonFabButton, IonIcon, IonInput, IonTitle, IonToolbar } from '@ionic/re
import { add } from 'ionicons/icons';
import React, { useState } from 'react';
import { useHistory } from 'react-router';
import { db, timestamp, arrayUnion } from '../services/firebase';
import { db, arrayUnion } from '../services/firebase';
import './RoomHeader.css';
type RoomHeaderProps = {
@ -25,7 +25,7 @@ const RoomHeader: React.FC<RoomHeaderProps> = ({ roomId, userId, ownerId }) => {
.collection('rooms')
.doc(roomId)
.update({
requests: arrayUnion({ createdAt: timestamp, senderId: userId, type: 'change' }),
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'change' }),
});
setVideoUrl('');

@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
import { db, timestamp, arrayUnion } from '../services/firebase';
import { db, arrayUnion } from '../services/firebase';
import { SYNC_MARGIN } from '../services/utilities';
type VideoPlayerProps = {
@ -21,12 +21,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
if (ownerId === userId) {
const currTime = player.current?.getCurrentTime();
if (currTime !== undefined) {
await db.collection('states').doc(roomId).update({
isPlaying: true,
time: currTime,
});
await db
.collection('rooms')
.doc(roomId)
.update({
requests: arrayUnion({ createdAt: timestamp, senderId: userId, type: 'play' }),
state: { isPlaying: true, time: currTime },
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: currTime, type: 'play' }),
});
}
}
@ -38,12 +42,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
if (ownerId === userId) {
const currTime = player.current?.getCurrentTime();
if (currTime !== undefined) {
await db.collection('states').doc(roomId).update({
isPlaying: false,
time: currTime,
});
await db
.collection('rooms')
.doc(roomId)
.update({
requests: arrayUnion({ createdAt: timestamp, senderId: userId, type: 'pause' }),
state: { isPlaying: false, time: currTime },
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: currTime, type: 'pause' }),
});
}
}
@ -52,11 +60,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
// Request an update after buffering is finished (member only)
const onBufferEnd = () => {
if (ownerId !== userId) {
db.collection('rooms').doc(roomId).collection('messages').add({
createdAt: timestamp,
senderId: userId,
type: 'updateState',
});
db.collection('rooms')
.doc(roomId)
.update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'updateState' }),
});
}
};
@ -85,7 +93,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
player.current?.seekTo(realTimeState);
roomRef.update({
requests: arrayUnion({ createdAt: timestamp, senderId: userId, type: 'updateState' }),
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'updateState' }),
});
}
}
@ -108,7 +116,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
const requests = docSnapshot.data()?.requests;
const req = requests[requests.length - 1];
if (req.type === 'updateState' && req.senderId !== userId) {
if (!!req && req.type === 'updateState' && req.senderId !== userId) {
const currTime = player.current?.getCurrentTime();
if (currTime !== undefined) {
stateRef.update({
@ -127,7 +135,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
// Listen for video URL changes
useEffect(() => {
const playlistRef = db.collection('playlist').doc(roomId);
const playlistRef = db.collection('playlists').doc(roomId);
const playlistUnsubscribe = playlistRef.onSnapshot((docSnapshot) => {
const data = docSnapshot.data();
if (data !== undefined) {

@ -17,14 +17,20 @@ const Home: React.FC = () => {
const roomId = await db.collection('rooms').add({
createdAt: timestamp,
ownerId: userId,
playlist: [{ createdAt: timestamp, url: 'https://www.youtube.com/watch?v=ksHOjnopT_U' }],
requests: [],
state: { time: 0, isPlaying: false },
});
await db.collection('chats').doc(roomId.id).set({
messages: [],
});
await db.collection('playlists').doc(roomId.id).set({
createdAt: timestamp,
url: 'https://www.youtube.com/watch?v=XEfDYMngJeE',
});
await db.collection('states').doc(roomId.id).set({
isPlaying: false,
time: 0,
});
// RealTimeDB preparations
await rtdb.ref('/rooms/' + roomId.id).set({ userCount: 0 });

@ -17,7 +17,7 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
const [ownerId, setOwnerId] = useState('undefined');
const [loading, setLoading] = useState(true);
const [userCount, setUserCount] = useState(0);
const [userList, setUserList] = useState<string[]>(['']);
const [userList, setUserList] = useState<Map<string, string>>(new Map<string, string>());
// Handle logging in
useEffect(() => {
@ -63,13 +63,13 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
// Keep track of online user presence in realtime database rooms
roomRef.on('value', async (snapshot) => {
// Populate list of users in a room
const set: string[] = [];
const map: Map<string, string> = new Map<string, string>();
snapshot.forEach((childSnapshot) => {
if (childSnapshot.key !== 'userCount') {
set.push(childSnapshot.child('name').val());
if (childSnapshot.key !== null && childSnapshot.key !== 'userCount') {
map.set(childSnapshot.key, childSnapshot.child('name').val());
}
});
setUserList(set);
setUserList(map);
if (!snapshot.hasChild(userId)) {
// Keep userId in the room as long as a connection from the client exists

Loading…
Cancel
Save