Skip to content

Commit

Permalink
Wiring the views with the server. r=samgiles (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmarty authored Jul 4, 2016
1 parent 2841254 commit f59075e
Show file tree
Hide file tree
Showing 15 changed files with 885 additions and 52 deletions.
2 changes: 1 addition & 1 deletion app/css/views/microphone.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.microphone {
position: absolute;
position: fixed;
z-index: 1;
top: 8rem;
right: 9rem;
Expand Down
2 changes: 1 addition & 1 deletion app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ; connect-src * ; object-src 'none' ; img-src 'self' ; referrer no-referrer ;">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ; connect-src 'self' https://calendar.knilxof.org ; object-src 'none' ; img-src 'self' ; referrer no-referrer ;">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<meta name="theme-color" content="#0ca8a4">
<link rel="stylesheet" href="css/app.css">
Expand Down
21 changes: 16 additions & 5 deletions app/js/controllers/main.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import BaseController from './base';
import UsersController from './users';
import RemindersController from './reminders';

import SpeechController from '../lib/speech-controller';
import Server from '../lib/server/index';

import React from 'components/react';
import ReactDOM from 'components/react-dom';
import Microphone from '../views/microphone';

const p = Object.freeze({
controllers: Symbol('controllers'),
onHashChanged: Symbol('onHashChanged'),
speechController: Symbol('speechController'),
server: Symbol('server'),

onHashChanged: Symbol('onHashChanged'),
});

export default class MainController extends BaseController {
Expand All @@ -19,7 +23,8 @@ export default class MainController extends BaseController {

const mountNode = document.querySelector('.app-view-container');
const speechController = new SpeechController();
const options = { mountNode, speechController };
const server = new Server();
const options = { mountNode, speechController, server };

const usersController = new UsersController(options);
const remindersController = new RemindersController(options);
Expand All @@ -31,6 +36,7 @@ export default class MainController extends BaseController {
};

this[p.speechController] = speechController;
this[p.server] = server;

window.addEventListener('hashchange', this[p.onHashChanged].bind(this));
}
Expand All @@ -49,14 +55,19 @@ export default class MainController extends BaseController {
});

location.hash = '';

setTimeout(() => {
//location.hash = 'users/login';
location.hash = 'reminders';
}, 16);
if (this[p.server].isLoggedIn) {
location.hash = 'reminders';
} else {
location.hash = 'users/login';
}
});

ReactDOM.render(
React.createElement(Microphone, {
speechController: this[p.speechController],
server: this[p.server],
}), document.querySelector('.microphone')
);
}
Expand Down
1 change: 1 addition & 0 deletions app/js/controllers/reminders.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class RemindersController extends BaseController {
ReactDOM.render(
React.createElement(Reminders, {
speechController: this.speechController,
server: this.server,
}), this.mountNode
);
}
Expand Down
9 changes: 6 additions & 3 deletions app/js/controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ export default class UsersController extends BaseController {

login() {
ReactDOM.render(
React.createElement(UserLogin, {}), this.mountNode
React.createElement(UserLogin, { server: this.server }), this.mountNode
);
}

logout() {
// Once logged out, we redirect to the login page.
location.hash = '#users/login';
this.server.logout()
.then(() => {
// Once logged out, we redirect to the login page.
location.hash = 'users/login';
});
}
}
159 changes: 159 additions & 0 deletions app/js/lib/server/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
'use strict';

const p = Object.freeze({
settings: Symbol('settings'),
net: Symbol('net'),

// Private methods.
getURL: Symbol('getURL'),
onceOnline: Symbol('onceOnline'),
onceReady: Symbol('onceReady'),
getChannelValues: Symbol('getChannelValues'),
updateChannelValue: Symbol('updateChannelValue'),
});

/**
* Instance of the API class is intended to abstract consumer from the API
* specific details (e.g. API base URL and version). It also tracks
* availability of the network, API host and whether correct user session is
* established. If any of this conditions is not met all API requests are
* blocked until it's possible to perform them, so consumer doesn't have to
* care about these additional checks.
*/
export default class API {
constructor(net, settings) {
this[p.net] = net;
this[p.settings] = settings;

Object.freeze(this);
}

/**
* Performs HTTP 'GET' API request and accepts JSON as response.
*
* @param {string} path Specific API resource path to be used in conjunction
* with the base API path.
* @return {Promise}
*/
get(path) {
return this[p.onceReady]()
.then(() => this[p.net].fetchJSON(this[p.getURL](path)));
}

/**
* Performs HTTP 'POST' API request and accepts JSON as response.
*
* @param {string} path Specific API resource path to be used in conjunction
* with the base API path.
* @param {Object=} body Optional object that will be serialized to JSON
* string and sent as 'POST' body.
* @return {Promise}
*/
post(path, body) {
console.log(path, body);

return this[p.onceReady]()
.then(() => this[p.net].fetchJSON(this[p.getURL](path), 'POST', body));
}

/**
* Performs HTTP 'PUT' API request and accepts JSON as response.
*
* @param {string} path Specific API resource path to be used in conjunction
* with the base API path.
* @param {Object=} body Optional object that will be serialized to JSON
* string and sent as 'PUT' body.
* @return {Promise}
*/
put(path, body) {
return this[p.onceReady]()
.then(() => this[p.net].fetchJSON(this[p.getURL](path), 'PUT', body));
}

/**
* Performs HTTP 'DELETE' API request and accepts JSON as response.
*
* @param {string} path Specific API resource path to be used in conjunction
* with the base API path.
* @param {Object=} body Optional object that will be serialized to JSON
* string and sent as 'DELETE' body.
* @return {Promise}
*/
delete(path, body) {
return this[p.onceReady]()
.then(() => this[p.net].fetchJSON(this[p.getURL](path), 'DELETE', body));
}

/**
* Performs either HTTP 'GET' or 'PUT' (if body parameter is specified) API
* request and accepts Blob as response.
*
* @param {string} path Specific API resource path to be used in conjunction
* with the base API path.
* @param {Object=} body Optional object that will be serialized to JSON
* string and sent as 'PUT' body.
* @param {string=} accept Mime type of the Blob we expect as a response
* (default is image/jpeg).
* @return {Promise}
*/
blob(path, body, accept = 'image/jpeg') {
return this[p.onceReady]()
.then(() => {
if (body) {
return this[p.net].fetchBlob(
this[p.getURL](path), accept, 'PUT', body
);
}

return this[p.net].fetchBlob(this[p.getURL](path), accept);
});
}

/**
* Creates a fully qualified API URL based on predefined base origin, API
* version and specified resource path.
*
* @param {string} path Specific API resource path to be used in conjunction
* with the base API path and version.
* @return {string}
* @private
*/
[p.getURL](path) {
if (!path || typeof path !== 'string') {
throw new Error('Path should be a valid non-empty string.');
}

return `${this[p.net].origin}/api/v${this[p.settings].apiVersion}/${path}`;
}

/**
* Returns a promise that is resolved once API is ready to use (API host is
* online).
* In the future we can add more checks like:
* * User is authenticated
* * Document is visible
*
* @returns {Promise}
* @private
*/
[p.onceReady]() {
return Promise.all([
this[p.onceOnline](),
]);
}

/**
* Returns a promise that is resolved once API host is discovered and online.
*
* @returns {Promise}
* @private
*/
[p.onceOnline]() {
const net = this[p.net];
if (net.online) {
return Promise.resolve();
}

return new Promise((resolve) => net.once('online', () => resolve()));
}
}
105 changes: 105 additions & 0 deletions app/js/lib/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* global URLSearchParams */

'use strict';

import EventDispatcher from '../common/event-dispatcher';

import Settings from './settings';
import Network from './network';
import WebPush from './webpush';
import API from './api';
import Reminders from './reminders';

// Private members.
const p = Object.freeze({
// Private properties.
settings: Symbol('settings'),
net: Symbol('net'),
webPush: Symbol('webPush'),
api: Symbol('api'),
});

export default class Server extends EventDispatcher {
constructor({ settings, net } = {}) {
super(['online']);

// Private properties.
this[p.settings] = settings || new Settings();
this[p.net] = net || new Network(this[p.settings]);
this[p.api] = new API(this[p.net], this[p.settings]);
this[p.webPush] = new WebPush(this[p.api], this[p.settings]);

// Init
this.reminders = new Reminders(this[p.api], this[p.settings]);

this[p.net].on('online', (online) => this.emit('online', online));
this[p.webPush].on('message', (msg) => this.emit('push-message', msg));

window.server = this;

Object.seal(this);
}

/**
* Clear all data/settings stored on the browser. Use with caution.
*
* @param {boolean} ignoreServiceWorker
* @return {Promise}
*/
clear(ignoreServiceWorker = true) {
const promises = [this[p.settings].clear()];

if (!navigator.serviceWorker && !ignoreServiceWorker) {
promises.push(navigator.serviceWorker.ready
.then((registration) => registration.unregister()));
}

return Promise.all(promises);
}

get online() {
return this[p.net].online;
}

get isLoggedIn() {
return !!this[p.settings].session;
}

/**
* Authenticate a user.
*
* @param {string} user
* @param {string} password
* @return {Promise}
*/
login(user, password) {
return this[p.api].post('login', { user, password })
.then((res) => {
this[p.settings].session = res.token;
});
}

/**
* Log out the user.
*
* @return {Promise}
*/
logout() {
this[p.settings].session = null;
return Promise.resolve();
}

/**
* Ask the user to accept push notifications from the server.
* This method will be called each time that we log in, but will stop the
* execution if we already have the push subscription information.
*
* @param {boolean} resubscribe Parameter used for testing purposes, and
* follow the whole subscription process even if we already have a push
* subscription information.
* @return {Promise}
*/
subscribeToNotifications(resubscribe = false) {
return this[p.webPush].subscribeToNotifications(resubscribe);
}
}
Loading

0 comments on commit f59075e

Please sign in to comment.