changed About to Settings, added username change field

optimize-reads
Simon Huang 5 years ago
parent 8ddcaa8de2
commit 38600326df

10
package-lock.json generated

@ -1492,6 +1492,11 @@
"@hapi/hoek": "^8.3.0" "@hapi/hoek": "^8.3.0"
} }
}, },
"@hookform/error-message": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-0.0.4.tgz",
"integrity": "sha512-as5acnxkWuF5XrzHhZlzdXUk4zpHNzFLe+uGyaJtSrMbDVTvcNL62ESRK5DTVGPix8shJnt6G2LfrobHBPpvYg=="
},
"@iconify/icons-simple-icons": { "@iconify/icons-simple-icons": {
"version": "1.0.47", "version": "1.0.47",
"resolved": "https://registry.npmjs.org/@iconify/icons-simple-icons/-/icons-simple-icons-1.0.47.tgz", "resolved": "https://registry.npmjs.org/@iconify/icons-simple-icons/-/icons-simple-icons-1.0.47.tgz",
@ -12032,6 +12037,11 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
}, },
"react-hook-form": {
"version": "6.9.2",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.9.2.tgz",
"integrity": "sha512-vCPEbHVCRvsoqrQARgQ7a3VrXzqbFOO53gHFRdQzLzHMT9kxum3wfcSi8A1b49KPRsomvsqexH4tBUJMneEu+Q=="
},
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

@ -6,6 +6,7 @@
"@capacitor/android": "^2.4.0", "@capacitor/android": "^2.4.0",
"@capacitor/core": "2.4.0", "@capacitor/core": "2.4.0",
"@capacitor/ios": "^2.4.0", "@capacitor/ios": "^2.4.0",
"@hookform/error-message": "0.0.4",
"@ionic/react": "^5.0.7", "@ionic/react": "^5.0.7",
"@ionic/react-router": "^5.0.7", "@ionic/react-router": "^5.0.7",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
@ -24,6 +25,7 @@
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"react": "^16.13.0", "react": "^16.13.0",
"react-dom": "^16.13.0", "react-dom": "^16.13.0",
"react-hook-form": "^6.9.2",
"react-player": "^2.6.0", "react-player": "^2.6.0",
"react-router": "^5.1.2", "react-router": "^5.1.2",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",

@ -24,7 +24,7 @@ const collection = {
requests: [ requests: [
{ {
createdAt: 'timestamp', createdAt: 'timestamp',
time: '01:25:44', // Relevant for 'play', 'pause' types data: '01:25:44', // Contents of data depend on type of request
type: 'updateState', type: 'updateState',
senderId: 'userId', senderId: 'userId',
}, },

@ -1,42 +0,0 @@
import { IonCol, IonGrid, IonIcon, IonRow, IonRouterLink } from '@ionic/react';
import { logoGithub } from 'ionicons/icons';
import { Icon } from '@iconify/react';
import discordIcon from '@iconify/icons-simple-icons/discord';
import React from 'react';
import './About.css';
type AboutProps = {
pane: string;
};
const About: React.FC<AboutProps> = ({ pane }) => {
return (
<IonGrid style={{ display: pane === 'about' ? null : 'none' }} class="about-grid">
<IonRow>
<IonCol>
<span>Any feedback, questions, or issues? </span>
<span role="img" aria-label="Turtle">
🐢🐢
</span>
</IonCol>
</IonRow>
<IonRow class="externals-row">
<IonCol size="3"></IonCol>
<IonCol size="3">
<IonRouterLink href="https://github.com/shuang854/Turtle" target="_blank">
<IonIcon icon={logoGithub} class="about-icons"></IonIcon>
</IonRouterLink>
</IonCol>
<IonCol size="3">
<IonRouterLink href="https://discord.gg/NEw3Msu" target="_blank">
<Icon icon={discordIcon} className="about-icons"></Icon>
</IonRouterLink>
</IonCol>
<IonCol size="3"></IonCol>
</IonRow>
</IonGrid>
);
};
export default About;

@ -19,3 +19,10 @@ ion-fab-button {
--background: var(--ion-color-secondary-shade); --background: var(--ion-color-secondary-shade);
--background-activated: var(--ion-color-secondary); --background-activated: var(--ion-color-secondary);
} }
ion-list-header {
color: var(--ion-color-primary);
font-size: 20px;
border-bottom: 1px solid
var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))));
}

@ -1,10 +1,10 @@
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 Settings from './Settings';
type FrameProps = { type FrameProps = {
ownerId: string; ownerId: string;
@ -26,7 +26,7 @@ const Frame: React.FC<FrameProps> = ({ ownerId, roomId, userId, userList, joinTi
<IonSegmentButton value="online" onClick={() => setPane('online')}> <IonSegmentButton value="online" onClick={() => setPane('online')}>
<IonIcon icon={peopleOutline}></IonIcon> <IonIcon icon={peopleOutline}></IonIcon>
</IonSegmentButton> </IonSegmentButton>
<IonSegmentButton value="about" onClick={() => setPane('about')}> <IonSegmentButton value="settings" onClick={() => setPane('settings')}>
<IonIcon icon={informationCircleOutline}></IonIcon> <IonIcon icon={informationCircleOutline}></IonIcon>
</IonSegmentButton> </IonSegmentButton>
</IonSegment> </IonSegment>
@ -40,7 +40,7 @@ const Frame: React.FC<FrameProps> = ({ ownerId, roomId, userId, userList, joinTi
joinTime={joinTime} joinTime={joinTime}
></Messages> ></Messages>
<OnlineList pane={pane} roomId={roomId} userId={userId} userList={userList}></OnlineList> <OnlineList pane={pane} roomId={roomId} userId={userId} userList={userList}></OnlineList>
<About pane={pane}></About> <Settings pane={pane} roomId={roomId} userId={userId}></Settings>
</IonCard> </IonCard>
); );
}; };

@ -41,6 +41,7 @@
.message-toolbar { .message-toolbar {
padding-left: 5px; padding-left: 5px;
--background: var(--ion-color-light); --background: var(--ion-color-light);
border-top: 1px solid #777;
} }
.footer-ios ion-toolbar:first-of-type { .footer-ios ion-toolbar:first-of-type {

@ -68,11 +68,11 @@ const Messages: React.FC<MessagesProps> = ({ pane, ownerId, roomId, userId, user
for (const req of requests) { for (const req of requests) {
if (req.createdAt > joinTime && req.type !== 'updateState') { if (req.createdAt > joinTime && req.type !== 'updateState') {
arr.push({ arr.push({
content: processType(req.type, req.time), content: processType(req.type, req.data),
createdAt: req.createdAt, createdAt: req.createdAt,
id: req.senderId + req.createdAt, id: req.senderId + req.createdAt,
senderId: req.senderId, senderId: req.senderId,
type: 'system', type: req.type,
}); });
} }
} }
@ -86,16 +86,18 @@ const Messages: React.FC<MessagesProps> = ({ pane, ownerId, roomId, userId, user
}, [roomId, joinTime]); }, [roomId, joinTime]);
// Convert request type to message content // Convert request type to message content
const processType = (type: string, time: number): string => { const processType = (type: string, data: any): string => {
switch (type) { switch (type) {
case 'change': case 'change':
return 'changed the video.'; return 'changed the video.';
case 'join': case 'join':
return 'joined the room.'; return 'joined the room.';
case 'pause': case 'pause':
return 'paused the video at ' + secondsToTimestamp(time); return 'paused the video at ' + secondsToTimestamp(data);
case 'play': case 'play':
return 'played the video from ' + secondsToTimestamp(time); return 'played the video from ' + secondsToTimestamp(data);
case 'nameChange':
return data.prev + ' changed their name to ' + data.curr;
default: default:
return ''; return '';
} }
@ -170,7 +172,7 @@ const Messages: React.FC<MessagesProps> = ({ pane, ownerId, roomId, userId, user
return ( return (
<IonRow key={msg.id}> <IonRow key={msg.id}>
<IonCol size="auto" class="system-msg"> <IonCol size="auto" class="system-msg">
<span>{getName(msg.senderId) + ' ' + msg.content}</span> <span>{msg.type === 'nameChange' ? msg.content : getName(msg.senderId) + ' ' + msg.content}</span>
</IonCol> </IonCol>
</IonRow> </IonRow>
); );

@ -2,13 +2,6 @@
height: calc(100% - 162px + 52px); height: calc(100% - 162px + 52px);
} }
.online-header {
border-bottom: 1px solid var(--ion-color-primary);
color: var(--ion-color-primary);
font-size: 20px;
align-items: center;
}
.online-list { .online-list {
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;

@ -66,7 +66,7 @@ const OnlineList: React.FC<OnlineListProps> = ({ pane, roomId, userId, userList
return ( return (
<IonContent style={{ display: pane === 'online' ? null : 'none' }} class="online-content"> <IonContent style={{ display: pane === 'online' ? null : 'none' }} class="online-content">
<IonListHeader class="online-header">Online</IonListHeader> <IonListHeader>Online</IonListHeader>
<IonList class="online-list"> <IonList class="online-list">
{Array.from(userList.values()).map((user) => { {Array.from(userList.values()).map((user) => {
return ( return (
@ -78,7 +78,7 @@ const OnlineList: React.FC<OnlineListProps> = ({ pane, roomId, userId, userList
</IonList> </IonList>
<IonRow> <IonRow>
<IonCol class="clipboard-col"> <IonCol class="clipboard-col">
<IonListHeader class="online-header">Invite friends!</IonListHeader> <IonListHeader>Invite friends!</IonListHeader>
<IonToolbar class="clipboard-toolbar"> <IonToolbar class="clipboard-toolbar">
<IonInput readonly value={window.location.href} ref={inputRef} class="clipboard-input"></IonInput> <IonInput readonly value={window.location.href} ref={inputRef} class="clipboard-input"></IonInput>
<IonFabButton slot="end" size="small" onClick={copyLink} class="send-button"> <IonFabButton slot="end" size="small" onClick={copyLink} class="send-button">

@ -28,7 +28,7 @@ const ReactPlayerFrame: React.FC<ReactPlayerFrameProps> = ({ ownerId, userId, ro
db.collection('rooms') db.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, data: currTime, type: 'play' }),
}); });
} }
} }
@ -48,7 +48,7 @@ const ReactPlayerFrame: React.FC<ReactPlayerFrameProps> = ({ ownerId, userId, ro
db.collection('rooms') db.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, data: currTime, type: 'pause' }),
}); });
} }
} }
@ -88,7 +88,7 @@ const ReactPlayerFrame: React.FC<ReactPlayerFrameProps> = ({ ownerId, userId, ro
db.collection('rooms') db.collection('rooms')
.doc(roomId) .doc(roomId)
.update({ .update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'updateState' }), requests: arrayUnion({ createdAt: Date.now(), senderId: userId, data: 0, type: 'updateState' }),
}); });
} }
}; };
@ -119,7 +119,7 @@ const ReactPlayerFrame: React.FC<ReactPlayerFrameProps> = ({ ownerId, userId, ro
player.current?.seekTo(realTimeState); player.current?.seekTo(realTimeState);
roomRef.update({ roomRef.update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'updateState' }), requests: arrayUnion({ createdAt: Date.now(), senderId: userId, data: 0, type: 'updateState' }),
}); });
} }
} }

@ -58,7 +58,7 @@ const SubscriptionFrame: React.FC<SubscriptionFrameProps> = ({ ownerId, userId,
db.collection('rooms') db.collection('rooms')
.doc(roomId) .doc(roomId)
.update({ .update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: e.data.time, type: type }), requests: arrayUnion({ createdAt: Date.now(), senderId: userId, data: e.data.time, type: type }),
}); });
} }
}; };
@ -167,7 +167,7 @@ const SubscriptionFrame: React.FC<SubscriptionFrameProps> = ({ ownerId, userId,
seekTo(actual?.time); seekTo(actual?.time);
roomRef.update({ roomRef.update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'updateState' }), requests: arrayUnion({ createdAt: Date.now(), senderId: userId, data: 0, type: 'updateState' }),
}); });
} }
} }

@ -25,7 +25,7 @@ const RoomHeader: React.FC<RoomHeaderProps> = ({ roomId, userId, ownerId }) => {
.collection('rooms') .collection('rooms')
.doc(roomId) .doc(roomId)
.update({ .update({
requests: arrayUnion({ createdAt: Date.now(), senderId: userId, time: 0, type: 'change' }), requests: arrayUnion({ createdAt: Date.now(), senderId: userId, data: 0, type: 'change' }),
}); });
setVideoUrl(''); setVideoUrl('');

@ -1,6 +1,24 @@
.settings-header {
text-align: left;
}
.name-item {
font-size: 14px;
color: var(--ion-color-secondary);
}
.name-input {
color: #fff;
}
.error-message {
padding-top: 4px;
padding-left: 16px;
color: #e61a61;
}
.about-grid { .about-grid {
margin-top: 50px; margin-top: 20px;
max-width: 500px;
font-size: 20px; font-size: 20px;
font-style: bold; font-style: bold;
color: var(--ion-color-secondary); color: var(--ion-color-secondary);

@ -0,0 +1,130 @@
import { ErrorMessage } from '@hookform/error-message';
import discordIcon from '@iconify/icons-simple-icons/discord';
import { Icon } from '@iconify/react';
import {
IonCol,
IonContent,
IonGrid,
IonIcon,
IonInput,
IonItem,
IonLabel,
IonListHeader,
IonRouterLink,
IonRow,
IonToast,
} from '@ionic/react';
import { logoGithub } from 'ionicons/icons';
import React, { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { arrayUnion, db, rtdb } from '../services/firebase';
import './Settings.css';
type SettingsProps = {
pane: string;
roomId: string;
userId: string;
};
const Settings: React.FC<SettingsProps> = ({ pane, roomId, userId }) => {
const { control, errors, setValue, getValues } = useForm({ mode: 'onChange' });
const [showNameChange, setShowNameChange] = useState(false);
// Update databases with new username
const changeName = async () => {
const newName = getValues('username');
if (newName !== '') {
const snapshot = await db.collection('users').doc(userId).get();
const prevName = snapshot.data()?.name;
db.collection('users').doc(userId).update({
name: newName,
});
rtdb.ref('/rooms/' + roomId + '/' + userId).set({ name: newName });
// Send 'nameChange' request for all clients to get a message about the name change
db.collection('rooms')
.doc(roomId)
.update({
requests: arrayUnion({
createdAt: Date.now(),
senderId: userId,
data: { prev: prevName, curr: newName },
type: 'nameChange',
}),
});
setShowNameChange(true);
}
};
const onEnter = (e: React.KeyboardEvent<HTMLIonInputElement>) => {
if (e.key === 'Enter') {
if (!errors.username) {
changeName();
setValue('username', '');
}
}
};
return (
<IonContent style={{ display: pane === 'settings' ? null : 'none' }}>
<IonListHeader class="settings-header">Settings</IonListHeader>
<IonItem class="name-item">
<IonLabel>Change Username</IonLabel>
<Controller
name="username"
render={({ onChange, onBlur, value }) => (
<IonInput
onIonChange={onChange}
onKeyDown={(e) => onEnter(e)}
placeholder="New name"
maxlength={20}
value={value}
class="name-input"
/>
)}
control={control}
rules={{
minLength: { value: 4, message: '⚠ Must be at least 4 characters long' },
pattern: { value: /^\w+$/, message: '⚠ Must be alphanumeric' },
}}
></Controller>
</IonItem>
<ErrorMessage name="username" errors={errors} as="span" className="error-message"></ErrorMessage>
<IonGrid class="about-grid">
<IonRow>
<IonCol>
<span>Any feedback, questions, or issues? </span>
<span role="img" aria-label="Turtle">
🐢🐢
</span>
</IonCol>
</IonRow>
<IonRow class="externals-row">
<IonCol size="3"></IonCol>
<IonCol size="3">
<IonRouterLink href="https://github.com/shuang854/Turtle" target="_blank">
<IonIcon icon={logoGithub} class="about-icons"></IonIcon>
</IonRouterLink>
</IonCol>
<IonCol size="3">
<IonRouterLink href="https://discord.gg/NEw3Msu" target="_blank">
<Icon icon={discordIcon} className="about-icons"></Icon>
</IonRouterLink>
</IonCol>
<IonCol size="3"></IonCol>
</IonRow>
</IonGrid>
<IonToast
color="primary"
duration={2000}
isOpen={showNameChange}
onDidDismiss={() => setShowNameChange(false)}
position="top"
message="Username changed successfully"
></IonToast>
</IonContent>
);
};
export default Settings;

@ -64,7 +64,7 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
// Keep track of online user presence in realtime database rooms // Keep track of online user presence in realtime database rooms
roomRef.on('value', async (snapshot) => { roomRef.on('value', async (snapshot) => {
// Populate list of users in a room // Populate list of usernames in a room
const map: Map<string, string> = new Map<string, string>(); const map: Map<string, string> = new Map<string, string>();
snapshot.forEach((childSnapshot) => { snapshot.forEach((childSnapshot) => {
if (childSnapshot.key !== null && childSnapshot.key !== 'userCount') { if (childSnapshot.key !== null && childSnapshot.key !== 'userCount') {

Loading…
Cancel
Save