* fixed realtime database inconsistencies

* added join/leave toasts
optimize-reads
Simon Huang 6 years ago
parent 3494fc7329
commit 70bf0ac439

@ -1,19 +1,20 @@
import { IonCard, IonIcon, IonSegment, IonSegmentButton } from '@ionic/react'; import { IonCard, IonIcon, IonSegment, IonSegmentButton } from '@ionic/react';
import { chatboxOutline, informationCircleOutline, peopleOutline } from 'ionicons/icons'; import { chatboxOutline, informationCircleOutline, peopleOutline } from 'ionicons/icons';
import React, { useState } from 'react'; import React, { useState } from 'react';
import About from './About';
import './Frame.css'; import './Frame.css';
import Messages from './Messages'; import Messages from './Messages';
import OnlineList from './OnlineList'; import OnlineList from './OnlineList';
import About from './About';
type FrameProps = { type FrameProps = {
ownerId: string; ownerId: string;
roomId: string; roomId: string;
userId: string; userId: string;
userList: Map<string, string>; userList: Map<string, string>;
joinTime: number;
}; };
const Frame: React.FC<FrameProps> = ({ ownerId, roomId, userId, userList }) => { const Frame: React.FC<FrameProps> = ({ ownerId, roomId, userId, userList, joinTime }) => {
const [pane, setPane] = useState('chat'); const [pane, setPane] = useState('chat');
return ( return (
@ -30,8 +31,15 @@ const Frame: React.FC<FrameProps> = ({ ownerId, roomId, userId, userList }) => {
</IonSegmentButton> </IonSegmentButton>
</IonSegment> </IonSegment>
<Messages pane={pane} ownerId={ownerId} roomId={roomId} userId={userId} userList={userList}></Messages> <Messages
<OnlineList pane={pane} userList={userList}></OnlineList> pane={pane}
ownerId={ownerId}
roomId={roomId}
userId={userId}
userList={userList}
joinTime={joinTime}
></Messages>
<OnlineList pane={pane} roomId={roomId} userId={userId} userList={userList}></OnlineList>
<About pane={pane}></About> <About pane={pane}></About>
</IonCard> </IonCard>
); );

@ -21,6 +21,7 @@ type MessagesProps = {
roomId: string; roomId: string;
userId: string; userId: string;
userList: Map<string, string>; userList: Map<string, string>;
joinTime: number;
}; };
type Message = { type Message = {
@ -30,8 +31,7 @@ type Message = {
senderId: string; senderId: string;
}; };
const Messages: React.FC<MessagesProps> = ({ pane, ownerId, roomId, userId, userList }) => { const Messages: React.FC<MessagesProps> = ({ pane, ownerId, roomId, userId, userList, joinTime }) => {
const [joinTime] = useState(Date.now()); // Time at mounting of the component
const [chats, setChats] = useState<Message[]>([]); // All processed chat messages const [chats, setChats] = useState<Message[]>([]); // All processed chat messages
const [systemMessages, setSystemMessages] = useState<Message[]>([]); // All processed system messages const [systemMessages, setSystemMessages] = useState<Message[]>([]); // All processed system messages
const [allMessages, setAllMessages] = useState<Message[]>([]); // Combined array of chat and system messages const [allMessages, setAllMessages] = useState<Message[]>([]); // Combined array of chat and system messages

@ -1,34 +1,67 @@
import { import {
IonCol,
IonContent, IonContent,
IonFabButton, IonFabButton,
IonIcon,
IonInput, IonInput,
IonItem, IonItem,
IonLabel, IonLabel,
IonList, IonList,
IonListHeader, IonListHeader,
IonToolbar,
IonIcon,
IonRow, IonRow,
IonCol,
IonToast, IonToast,
IonToolbar,
} from '@ionic/react'; } from '@ionic/react';
import React, { useState, useRef } from 'react';
import './OnlineList.css';
import { clipboardOutline } from 'ionicons/icons';
import copy from 'copy-to-clipboard'; 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 = { type OnlineListProps = {
pane: string; pane: string;
roomId: string;
userId: string;
userList: Map<string, string>; userList: Map<string, string>;
}; };
const OnlineList: React.FC<OnlineListProps> = ({ pane, userList }) => { const OnlineList: React.FC<OnlineListProps> = ({ pane, roomId, userId, userList }) => {
const inputRef = useRef<HTMLIonInputElement>(null); const inputRef = useRef<HTMLIonInputElement>(null);
const [showToast, setShowToast] = useState(false); const connectionRef = useRef<HTMLIonToastElement>(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 = () => { const copyLink = () => {
copy(window.location.href); copy(window.location.href);
setShowToast(true); setShowCopied(true);
}; };
return ( return (
@ -55,11 +88,21 @@ const OnlineList: React.FC<OnlineListProps> = ({ pane, userList }) => {
</IonCol> </IonCol>
</IonRow> </IonRow>
<IonToast <IonToast
isOpen={showToast}
onDidDismiss={() => setShowToast(false)}
message="Room link copied"
color="primary" color="primary"
duration={2000} duration={2000}
isOpen={showCopied}
onDidDismiss={() => setShowCopied(false)}
position="top"
message="Room link copied"
></IonToast>
<IonToast
color="primary"
duration={500}
isOpen={showConnectionChange}
onDidDismiss={() => setShowConnectionChange(false)}
position="top"
message={connectionMessage}
ref={connectionRef}
></IonToast> ></IonToast>
</IonContent> </IonContent>
); );

@ -16,18 +16,17 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
const [allowUpdate, setAllowUpdate] = useState(true); const [allowUpdate, setAllowUpdate] = useState(true);
// Update database on play (owner only) // Update database on play (owner only)
const onPlay = async () => { const onPlay = () => {
setPlaying(true); setPlaying(true);
if (ownerId === userId) { if (ownerId === userId) {
const currTime = player.current?.getCurrentTime(); const currTime = player.current?.getCurrentTime();
if (currTime !== undefined) { if (currTime !== undefined) {
await db.collection('states').doc(roomId).update({ db.collection('states').doc(roomId).update({
isPlaying: true, isPlaying: true,
time: currTime, time: currTime,
}); });
await db db.collection('rooms')
.collection('rooms')
.doc(roomId) .doc(roomId)
.update({ .update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: currTime, type: 'play' }), requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: currTime, type: 'play' }),
@ -37,18 +36,17 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
}; };
// Update database on pause (owner only) // Update database on pause (owner only)
const onPause = async () => { const onPause = () => {
setPlaying(false); setPlaying(false);
if (ownerId === userId) { if (ownerId === userId) {
const currTime = player.current?.getCurrentTime(); const currTime = player.current?.getCurrentTime();
if (currTime !== undefined) { if (currTime !== undefined) {
await db.collection('states').doc(roomId).update({ db.collection('states').doc(roomId).update({
isPlaying: false, isPlaying: false,
time: currTime, time: currTime,
}); });
await db db.collection('rooms')
.collection('rooms')
.doc(roomId) .doc(roomId)
.update({ .update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: currTime, type: 'pause' }), requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: currTime, type: 'pause' }),

@ -11,6 +11,25 @@ const Home: React.FC = () => {
let history = useHistory(); 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 // Populate both Firestore and RealTimeDB before navigating to room
const createRoom = async () => { const createRoom = async () => {
// Firestore preparations // Firestore preparations
@ -37,25 +56,6 @@ const Home: React.FC = () => {
return history.push(path); 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 ( return (
<IonPage> <IonPage>
<IonHeader> <IonHeader>

@ -4,7 +4,7 @@ import { RouteComponentProps, useHistory } from 'react-router';
import Frame from '../components/Frame'; import Frame from '../components/Frame';
import RoomHeader from '../components/RoomHeader'; import RoomHeader from '../components/RoomHeader';
import VideoPlayer from '../components/VideoPlayer'; 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 { generateAnonName } from '../services/utilities';
import './Room.css'; import './Room.css';
@ -16,8 +16,8 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
const [userId, setUserId] = useState(''); const [userId, setUserId] = useState('');
const [ownerId, setOwnerId] = useState('undefined'); const [ownerId, setOwnerId] = useState('undefined');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [userCount, setUserCount] = useState(0);
const [userList, setUserList] = useState<Map<string, string>>(new Map<string, string>()); const [userList, setUserList] = useState<Map<string, string>>(new Map<string, string>());
const [joinTime] = useState(Date.now()); // Time at mounting of the component
// Verify that the roomId exists in db // Verify that the roomId exists in db
useEffect(() => { useEffect(() => {
@ -77,27 +77,26 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
// Keep userId in the room as long as a connection from the client exists // 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; const username = (await db.collection('users').doc(userId).get()).data()?.name;
await roomRef.child(userId).set({ name: username }); await roomRef.child(userId).set({ name: username });
await roomRef.update({ userCount: increment });
await db roomRef.child(userId).onDisconnect().cancel(); // Clear any disconnect actions in case any are queued
.collection('rooms') roomRef.child(userId).onDisconnect().remove(); // Remove userId from the room when disconnect happens
.doc(roomId)
.update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'join' }),
});
} }
}); });
roomRef.child('userCount').on('value', (snapshot) => { // Manage user count
setUserCount(snapshot.val()); 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 // Remove room availability when the last person leaves
availableRef.on('value', async (snapshot) => { roomRef.child('userCount').on('value', (snapshot) => {
if (!snapshot.hasChild(roomId)) { if (snapshot.val() <= 1) {
await availableRef.child(roomId).set({ availableRef.onDisconnect().remove();
name: 'Room Name', } else {
createdAt: new Date().toISOString(), availableRef.onDisconnect().cancel();
});
} }
}); });
@ -119,37 +118,6 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
} }
}, [userId, validRoom, roomId]); }, [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 ( return (
<IonPage> <IonPage>
<IonHeader> <IonHeader>
@ -164,7 +132,7 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
<VideoPlayer ownerId={ownerId} userId={userId} roomId={roomId}></VideoPlayer> <VideoPlayer ownerId={ownerId} userId={userId} roomId={roomId}></VideoPlayer>
</IonCol> </IonCol>
<IonCol size="12" sizeLg="3" class="frame-col"> <IonCol size="12" sizeLg="3" class="frame-col">
<Frame ownerId={ownerId} roomId={roomId} userId={userId} userList={userList}></Frame> <Frame ownerId={ownerId} roomId={roomId} userId={userId} userList={userList} joinTime={joinTime}></Frame>
</IonCol> </IonCol>
</IonRow> </IonRow>
</IonGrid> </IonGrid>

Loading…
Cancel
Save