implemented cloud functions and created chat UI mockup

pull/3/head
Simon Huang 5 years ago
parent f3efebc1dc
commit f4905e4d36

@ -0,0 +1,5 @@
{
"projects": {
"default": "turtle-95153"
}
}

1
.gitignore vendored

@ -22,3 +22,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
ui-debug.log*

@ -0,0 +1,8 @@
{
"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build"
]
}
}

@ -0,0 +1,12 @@
## Compiled JavaScript files
**/*.js
**/*.js.map
# Typescript v1 declaration files
typings/
node_modules/
lib/
firebase-debug.log
ui-debug.log

File diff suppressed because it is too large Load Diff

@ -0,0 +1,26 @@
{
"name": "functions",
"scripts": {
"lint": "tslint --project tsconfig.json",
"build": "tsc",
"serve": "npm run build && firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "10"
},
"main": "lib/index.js",
"dependencies": {
"firebase-admin": "^8.10.0",
"firebase-functions": "^3.6.1"
},
"devDependencies": {
"tslint": "^5.12.0",
"typescript": "^3.8.0",
"firebase-functions-test": "^0.2.0"
},
"private": true
}

@ -0,0 +1,27 @@
import functions = require('firebase-functions');
import admin = require('firebase-admin');
admin.initializeApp();
const firestore = admin.firestore();
// Start writing Firebase Functions
// https://firebase.google.com/docs/functions/typescript
export const helloWorld = functions.https.onRequest((request, response) => {
functions.logger.info('Hello logs!', { structuredData: true });
response.send('Hello from Firebase!');
});
export const deleteRoom = functions.database.ref('/available/{roomId}').onDelete(async (snapshot, context) => {
const firestoreRef = firestore.doc(`rooms/${context.params.roomId}`);
// // Consider fast changes to realtime database
// const currSnapshot = snapshot.val();
// const newSnapshot = await snapshot.ref.once('value');
// if (newSnapshot.val().last_changed > currSnapshot.last_changed) {
// return null;
// }
return firestoreRef.delete();
});

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2017"
},
"compileOnSave": true,
"include": [
"src"
]
}

@ -0,0 +1,115 @@
{
"rules": {
// -- Strict errors --
// These lint rules are likely always a good idea.
// Force function overloads to be declared together. This ensures readers understand APIs.
"adjacent-overload-signatures": true,
// Do not allow the subtle/obscure comma operator.
"ban-comma-operator": true,
// Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules.
"no-namespace": true,
// Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars.
"no-parameter-reassignment": true,
// Force the use of ES6-style imports instead of /// <reference path=> imports.
"no-reference": true,
// Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the
// code currently being edited (they may be incorrectly handling a different type case that does not exist).
"no-unnecessary-type-assertion": true,
// Disallow nonsensical label usage.
"label-position": true,
// Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }.
"no-conditional-assignment": true,
// Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed).
"no-construct": true,
// Do not allow super() to be called twice in a constructor.
"no-duplicate-super": true,
// Do not allow the same case to appear more than once in a switch block.
"no-duplicate-switch-case": true,
// Do not allow a variable to be declared more than once in the same block. Consider function parameters in this
// rule.
"no-duplicate-variable": [true, "check-parameters"],
// Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should
// instead use a separate variable name.
"no-shadowed-variable": true,
// Empty blocks are almost never needed. Allow the one general exception: empty catch blocks.
"no-empty": [true, "allow-empty-catch"],
// Functions must either be handled directly (e.g. with a catch() handler) or returned to another function.
// This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on.
"no-floating-promises": true,
// Do not allow any imports for modules that are not in package.json. These will almost certainly fail when
// deployed.
"no-implicit-dependencies": true,
// The 'this' keyword can only be used inside of classes.
"no-invalid-this": true,
// Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead.
"no-string-throw": true,
// Disallow control flow statements, such as return, continue, break, and throw in finally blocks.
"no-unsafe-finally": true,
// Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid();
"no-void-expression": [true, "ignore-arrow-function-shorthand"],
// Disallow duplicate imports in the same file.
"no-duplicate-imports": true,
// -- Strong Warnings --
// These rules should almost never be needed, but may be included due to legacy code.
// They are left as a warning to avoid frustration with blocked deploys when the developer
// understand the warning and wants to deploy anyway.
// Warn when an empty interface is defined. These are generally not useful.
"no-empty-interface": {"severity": "warning"},
// Warn when an import will have side effects.
"no-import-side-effect": {"severity": "warning"},
// Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for
// most values and let for values that will change.
"no-var-keyword": {"severity": "warning"},
// Prefer === and !== over == and !=. The latter operators support overloads that are often accidental.
"triple-equals": {"severity": "warning"},
// Warn when using deprecated APIs.
"deprecation": {"severity": "warning"},
// -- Light Warnings --
// These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info"
// if TSLint supported such a level.
// prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array.
// (Even better: check out utils like .map if transforming an array!)
"prefer-for-of": {"severity": "warning"},
// Warns if function overloads could be unified into a single function with optional or rest parameters.
"unified-signatures": {"severity": "warning"},
// Prefer const for values that will not change. This better documents code.
"prefer-const": {"severity": "warning"},
// Multi-line object literals and function calls should have a trailing comma. This helps avoid merge conflicts.
"trailing-comma": {"severity": "warning"}
},
"defaultSeverity": "error"
}

@ -0,0 +1,30 @@
// Database schema
// Firestore
const collection = {
rooms: {
roomId: {
createdAt: 'timestamp',
ownerId: 'userId',
},
},
};
// Realtime Database - needed for tracking user presence
const turtle = {
// Needed for database triggers updating Firestore
available: {
roomId: {
createdAt: '2020-08-12T00:13:16.273Z',
},
},
// Keeping track of which users are present in a room
rooms: {
roomId: {
userId: {
name: 'Username',
},
},
},
};

@ -0,0 +1,42 @@
ion-grid {
border-left: solid 1px #bbb;
border-right: solid 1px #bbb;
height: 100%;
}
ion-col {
padding: 10px;
border-radius: 10px;
margin-bottom: 4px;
white-space: pre-wrap;
max-width: 75%;
}
ion-textarea {
border: solid 1px #999;
}
.right-align {
justify-content: flex-end;
}
.my-msg {
text-align: right;
background: var(--ion-color-primary);
color: #fff;
}
.other-msg {
background: var(--ion-color-secondary);
color: #fff;
}
.send-msg {
align-items: center;
justify-content: center;
display: flex;
}
.send-button {
width: 100%;
}

@ -0,0 +1,54 @@
import { IonButton, IonCol, IonContent, IonFooter, IonGrid, IonRow, IonTextarea } from '@ionic/react';
import React from 'react';
import './Chat.css';
const Chat: React.FC = () => {
return (
<>
<IonContent>
<IonGrid>
<IonRow class="right-align">
<IonCol size="auto" class="my-msg">
<b>Simon: </b>
<span>Hello!</span>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="auto" class="other-msg">
<b>Manuel: </b>
<span>They are all asking me about what I'm going to do next</span>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="auto" class="other-msg">
<b>Amaria: </b>
<span>Probably riding on</span>
</IonCol>
</IonRow>
<IonRow class="right-align">
<IonCol size="auto" class="my-msg">
<b>Simon: </b>
<span>my photo jet</span>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
<IonFooter class="ion-no-border">
<IonGrid>
<IonRow>
<IonCol size="9">
<IonTextarea></IonTextarea>
</IonCol>
<IonCol size="3" class="send-msg">
<IonButton expand="block" color="primary" class="send-button">
Send
</IonButton>
</IonCol>
</IonRow>
</IonGrid>
</IonFooter>
</>
);
};
export default Chat;

@ -1,4 +1,4 @@
ion-button {
.create-room {
text-align: center;
justify-content: center;
margin: 30% 10% 0 10%;

@ -18,6 +18,7 @@ const Home: React.FC = () => {
});
await rtdb.ref('/rooms/' + roomId.id).set({ userCount: 0 });
await rtdb.ref('/available/' + roomId.id).set({ name: 'Room Name', createdAt: new Date().toISOString() });
const path = '/room/' + roomId.id;
return history.push(path);
};
@ -51,7 +52,9 @@ const Home: React.FC = () => {
{loading ? (
<IonContent className="ion-padding">Loading...</IonContent>
) : (
<IonButton onClick={createRoom}>Create Room</IonButton>
<IonButton onClick={createRoom} class="create-room">
Create Room
</IonButton>
)}
</IonRow>
</IonGrid>

@ -2,6 +2,7 @@ import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/rea
import React, { useEffect, useState } from 'react';
import { RouteComponentProps, useHistory } from 'react-router';
import { auth, db, rtdb, increment, decrement } from '../services/firebase';
import Chat from '../components/Chat';
const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
const history = useHistory();
@ -46,29 +47,43 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
useEffect(() => {
if (!didConnect && userId !== '' && validRoom) {
const populateRoom = () => {
const ref = rtdb.ref('/rooms/' + roomId);
const roomRef = rtdb.ref('/rooms/' + roomId);
const availableRef = rtdb.ref('/available/');
ref.on('value', async (snapshot) => {
roomRef.on('value', async (snapshot) => {
if (!snapshot.hasChild(userId)) {
// Keep userId in the room as long as a connection from the client exists
await ref.child(userId).set({ name: 'placeholder' });
await ref.update({ userCount: increment });
await roomRef.child(userId).set({ name: 'placeholder' });
await roomRef.update({ userCount: increment });
}
});
ref.child('userCount').on('value', (snapshot) => {
roomRef.child('userCount').on('value', (snapshot) => {
setUserCount(snapshot.val());
});
// Re-add room into /available/ if the room was deleted
availableRef.on('child_removed', async (snapshot) => {
if (!snapshot.hasChild(roomId)) {
await availableRef.child(roomId).set({
name: 'Room Name',
createdAt: new Date().toISOString(),
});
}
});
setLoading(false); // Ready when connection to rtdb is made
// Unsubscribe listeners
return () => {
ref.off('value');
ref.child('userCount').off('value');
roomRef.off('value');
roomRef.child('userCount').off('value');
availableRef.off('child_removed');
};
};
populateRoom();
setDidConnect(true); // Run this effect only once
setDidConnect(true); // Run this useEffect only once
}
}, [userId, validRoom, roomId, userCount, loading, didConnect]);
@ -78,6 +93,7 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
const depopulateRoom = async () => {
const refUser = rtdb.ref('/rooms/' + roomId + '/' + userId);
const refRoom = rtdb.ref('/rooms/' + roomId);
const refAvailable = rtdb.ref('/available/' + roomId);
// Always remove user from room on disconnect
await refRoom.onDisconnect().update({ userCount: decrement });
@ -87,9 +103,11 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
console.log('userCount: ' + userCount);
if (userCount <= 1) {
await refRoom.onDisconnect().remove();
await refAvailable.onDisconnect().remove();
} else {
await refRoom.onDisconnect().cancel(); // Cancels all disconnect actions
await refRoom.onDisconnect().update({ userCount: decrement });
await refRoom.onDisconnect().cancel(); // Cancels all disconnect actions at and under refRoom
await refAvailable.onDisconnect().cancel();
await refRoom.onDisconnect().update({ userCount: decrement }); // User disconnect still needs to be handled
await refUser.onDisconnect().remove();
}
};
@ -105,11 +123,7 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
<IonTitle>Turtle</IonTitle>
</IonToolbar>
</IonHeader>
{loading ? (
<IonContent className="ion-padding">Loading...</IonContent>
) : (
<IonContent className="ion-padding">Video and chat</IonContent>
)}
{loading ? <IonContent className="ion-padding">Loading...</IonContent> : <Chat></Chat>}
</IonPage>
);
};

Loading…
Cancel
Save