Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notifications for duels #189

Draft
wants to merge 48 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
78bfb7f
add message
Feb 11, 2021
4cf4661
update style
Feb 11, 2021
fbdee43
CSS in progress
Feb 15, 2021
1e951dc
Improve css
Feb 15, 2021
da530cd
Improve Notification
Feb 15, 2021
ebdaab8
Ask user
Feb 15, 2021
747f55b
Fix tests
Feb 17, 2021
e139182
Improve notification handle
Feb 17, 2021
f2f23d8
can send notification when Push ation
Feb 22, 2021
593a1e4
subscribe to push
Feb 24, 2021
cacb85f
fix notification state
Feb 24, 2021
79f1cef
use firebase
Feb 24, 2021
4193af9
get notifications from firebase
Feb 25, 2021
7b7abc2
add messaging
Feb 26, 2021
0bda59c
send messages from server
Feb 26, 2021
cf5e197
merge
Feb 26, 2021
81f5c4d
Merge branch 'main' into notif-handle
Feb 26, 2021
e06d759
update database to save messaging token
Feb 27, 2021
a6c78fc
add request
Feb 27, 2021
143661c
update messaging token with api
Feb 27, 2021
cf76597
add or remove messaging token (api)
Feb 27, 2021
b8ca9d0
save token
Feb 27, 2021
16315ce
update client
Feb 27, 2021
45ad303
add handler
Feb 27, 2021
880ee9d
send notification when a new duel is available
Feb 27, 2021
61d8d94
update workflow
Feb 27, 2021
d1c8682
attention ça va bientôt merge
Feb 28, 2021
d029fca
merge
Feb 28, 2021
53c0881
update to utils
Feb 28, 2021
af950a4
can send notifications
Feb 28, 2021
ab4b410
foreground notification
Feb 28, 2021
2956803
notification with timer
Feb 28, 2021
17c1498
get foreground notification
Feb 28, 2021
4b80f9a
fix css
Feb 28, 2021
a44dcea
bye bye console.log
Feb 28, 2021
00b92b5
merge
Feb 28, 2021
5450bb5
fix server tests
Feb 28, 2021
9dd31ba
upda
Feb 28, 2021
d41d8e6
update client tests
Feb 28, 2021
55232c7
fix some bugs
Mar 1, 2021
6331daa
nathal suggestions
Mar 1, 2021
fc363e5
merge
Mar 9, 2021
7be4263
merge again
Mar 9, 2021
8c8fa44
Seems to be working, I mean the merge, of course, haha, what a long n…
Mar 9, 2021
6b9f156
Update route
Mar 9, 2021
6d203e2
add async / await
Mar 9, 2021
33413f7
Looks like it's working
Mar 9, 2021
62f7eab
add doc
Apr 5, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ rapport-guacamole/
.eslintcache
server/logs/
server/files/
server/files-test/
uploads/
tmp/
server/files-test/
tmp/
serviceAccountKey.json
7,475 changes: 2,752 additions & 4,723 deletions client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dirty-chai": "^2.0.1",
"dotenv": "^8.2.0",
"find-config": "^1.0.0",
"firebase": "^8.2.9",
"node-sass": "^4.14.1",
"path": "^0.12.7",
"prop-types": "^15.7.2",
Expand Down
77 changes: 75 additions & 2 deletions client/src/App.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReloadIcon } from "@modulz/radix-icons";
import { ReloadIcon, BellIcon } from "@modulz/radix-icons";
import axios from "axios";
import PropTypes from "prop-types";
import React, { lazy, Suspense, Component } from "react";
Expand All @@ -7,6 +7,9 @@ import { ReactQueryDevtools } from "react-query/devtools";
import { BrowserRouter as Router, Route, Switch, useLocation } from "react-router-dom";

import "./styles/styles.scss";
import ButtonFullWidth from "./components/buttons/ButtonFullWidth";
import FullScreenMessage from "./components/FullScreenMessage";
import NotificationForeground from "./components/NotificationForeground";
import ProtectedRoute from "./components/ProtectedRoute";
import Loading from "./components/status/Loading";
import TopBar from "./components/system/TopBar";
Expand All @@ -16,6 +19,7 @@ import Menu from "./pages/Menu";
import AuthService from "./services/auth.service";
import * as serviceWorker from "./serviceWorker";
import queryClient from "./utils/configuredQueryClient";
import { getToken, checkAndRemoveToken, onMessage } from "./utils/messaging";

const About = lazy(() => import("./pages/About"));
const Admin = lazy(() => import("./pages/Admin"));
Expand Down Expand Up @@ -89,6 +93,8 @@ export default class App extends Component {
waitingServiceWorker: null,
isUpdateAvailable: false,
installPromptEvent: null,
requireNotificationPermission: false,
foregroundNotification: null,
user: pseudo || null,
};
}
Expand Down Expand Up @@ -117,8 +123,40 @@ export default class App extends Component {
installPromptEvent: event,
});
});

/*
* Request permission for notifications and subscribe to push
*
* The user must be logged in. We obtain his push registration
* token which must then be registered in the database and in localStorage.
*/
if (this.state.user) {
getToken(this.state.user)
.then((token) => console.log(token))
.catch((err) => console.error("Error: can't retrieve token", err));
} else {
// If the user is not logged in, check if there is a token in the localstorage and delete it
checkAndRemoveToken().catch((err) => console.error("Error: can't remove token", err));
}

// Listen for notifications (in foreground)
// Function given by the Firebase API
onMessage().then((payload) => {
// A notification must have a title
if (!payload.data.title) {
console.error("Can't display notification without title");
return;
}

// Add the notification to the state
const { title, body } = payload.data;
this.setState({ foregroundNotification: { title: title, body: body } });
});
}

/**
* Send a message to the service-worker to request the new version
*/
updateServiceWorker = () => {
this.setState({ updateRequired: true });

Expand All @@ -127,7 +165,14 @@ export default class App extends Component {
};

render() {
const { user, isUpdateAvailable, installPromptEvent, updateRequired } = this.state;
const {
user,
isUpdateAvailable,
installPromptEvent,
updateRequired,
requireNotificationPermission,
foregroundNotification,
} = this.state;

return (
<QueryClientProvider client={queryClient}>
Expand All @@ -137,6 +182,34 @@ export default class App extends Component {
<UpdateButton updateSW={this.updateServiceWorker} updateRequired={updateRequired} />
)}

{/* Display a screen to ask the user permission for notifications */}
{requireNotificationPermission && (
<FullScreenMessage id="authorization-notification">
<BellIcon />

<h1>Notifications</h1>

<p>
Nous aimerions vous envoyer des notifications pour vous prévenir lorsque de nouveaux
duels sont disponibles.
</p>

<ButtonFullWidth onClick={this.displayBrowserNotificationPermission}>
Autoriser les notifications
</ButtonFullWidth>
<button onClick={this.displayBrowserNotificationPermission}>Ne pas autoriser</button>
</FullScreenMessage>
)}

{/* If there is a notification in foreground, display it */}
{foregroundNotification !== null && (
<NotificationForeground
title={foregroundNotification.title}
text={foregroundNotification.body}
closeNotification={() => this.setState({ foregroundNotification: null })}
/>
)}

<Suspense fallback={<Loading />}>
<Switch>
<Route path="/" exact>
Expand Down
15 changes: 15 additions & 0 deletions client/src/components/FullScreenMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PropTypes } from "prop-types";
import React from "react";

const FullScreenMessage = ({ id, children }) => (
<section className="fullscreen-message" id={id}>
<div>{children}</div>
</section>
);

FullScreenMessage.propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.array.isRequired,
};

export default FullScreenMessage;
40 changes: 40 additions & 0 deletions client/src/components/NotificationForeground.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Cross1Icon } from "@modulz/radix-icons";
import { PropTypes } from "prop-types";
import React, { useState, useEffect } from "react";

const NotificationForeground = ({ title, text, closeNotification }) => {
const [timer, setTimer] = useState(10);

useEffect(() => {
let interval = setTimeout(() => {
setTimer(timer - 0.05);
}, 50);

if (timer <= 0) {
closeNotification();
}

return () => clearInterval(interval);
});

return (
<div className="notification">
<div id="indicator" style={{ width: `${timer * 10}%` }}></div>

<div id="close" onClick={closeNotification}>
<Cross1Icon />
</div>

<h2>{title}</h2>
<p>{text}</p>
</div>
);
};

NotificationForeground.propTypes = {
title: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
closeNotification: PropTypes.func.isRequired,
};

export default NotificationForeground;
2 changes: 1 addition & 1 deletion client/src/pages/Train.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class PlayView extends Component {
<Question numero={questionNum} text={question.wording} />

{inProgress ? (
<Timer inProgress={inProgress} duration={timer} updateParent={this.updateTimer} />
<Timer inProgress={inProgress} duration={timer ?? 0} updateParent={this.updateTimer} />
) : (
<div id="next-btn">
<ButtonCircle onClick={this.nextQuestion}>
Expand Down
72 changes: 70 additions & 2 deletions client/src/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
/* eslint-disable no-restricted-globals */

// Import Firebase scripts
import firebase from "firebase";

// Import firebase scripts
self.importScripts("https://www.gstatic.com/firebasejs/8.2.9/firebase-app.js");
self.importScripts("https://www.gstatic.com/firebasejs/8.2.9/firebase-messaging.js");
Comment on lines +7 to +8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this possible to have these file in our bundle instead of downloading them from a third-party server?


let cacheName = "guacamolePWA-v1";
caches.has(cacheName).then((res) => {
if (res) {
cacheName = "guacamolePWA-v2";
}
});

// Initialize app with firebase data
// You can create a project and get these data here: https://firebase.google.com/
firebase.initializeApp({
apiKey: "YOUR_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
});

/**
* Install the service worker
* JavaScript files and assets (e.g. images) are cached
Expand Down Expand Up @@ -65,16 +83,66 @@ self.addEventListener("fetch", (e) => {
const newResource = r.clone();
caches.open(cacheName).then((cache) => {
console.info(`[Service Worker] Caching new resource: ${e.request.url}`);
cache.put(e.request, newResource);
cache.put(e.request, newResource).catch(() => console.error("Can't cache new resource"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an error or a simple warning?

});
return r;
});
})
);
});

/**
* Message sent from the client to the service-worker
*/
self.addEventListener("message", (e) => {
// User wants to use the new sw
if (e.data.type && e.data.type === "SKIP_WAITING") {
self.skipWaiting();
self.skipWaiting().catch(() => console.error("Can't skip waiting"));
}
});

/**
* Push message handling (app on background)
* Use functions from firebase API
*/
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
// A notification must have a title
if (!payload.data.title) {
console.error("Can't display notification without title");
return;
}

const { title } = payload.data;
const options = {
body: payload.data.body,
icon: payload.data.image,
data: payload.data,
};

// Show notification
self.registration
.showNotification(title, options)
.catch((err) => console.error("Can't send notification: ", err));
});

/**
* Handle user click on a notification
* Display the duel page id linked to the notification
*/
self.addEventListener("notificationclick", (e) => {
const { notification, action } = e;
if (action === "close") {
notification.close();
return;
}

const { data } = notification;
if (data.type === "duel" && data.duelId) {
// eslint-disable-next-line no-undef
clients
.openWindow(`/duel/${data.duelId}`)
.catch(() => console.error("Can't open window after the user clicks on a notification"));
notification.close();
}
});
17 changes: 17 additions & 0 deletions client/src/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,20 @@ import sinonChai from "sinon-chai";

chai.use(dirtyChai).use(createChaiJestDiff()).use(createChaiEnzyme()).use(sinonChai);
configureEnzyme({ adapter: new Adapter() });

/* Mock global variables */

const Notification = {
permission: "granted",
};

const serviceWorker = {
getRegistration() {
return new Promise((resolve) => {
resolve(undefined);
});
},
};

global.Notification = Notification;
global.navigator.serviceWorker = serviceWorker;
2 changes: 1 addition & 1 deletion client/src/styles/base/_App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ main {
box-sizing: border-box;
}

@media only screen and (min-device-width: $screen-desktop) {
@media only screen and (min-width: $screen-desktop) {
body {
display: flex;
justify-content: center;
Expand Down
2 changes: 2 additions & 0 deletions client/src/styles/base/_variables.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ $color-red-error: #e23737;

/* ---------- Avatar ---------- */
/** These should match the number of possibilities in images/avatar/files.png */

$number-choices-eyes: 5;
$number-choices-hands: 5;
$number-choices-hats: 5;
$number-choices-mouthes: 5;

/* ---------- Make variables accessible from JS files ---------- */

:export {
numberChoicesEyes: $number-choices-eyes;
numberChoicesHands: $number-choices-hands;
Expand Down
Loading