Skip to content

Commit

Permalink
feat: Webapp routing (#6195)
Browse files Browse the repository at this point in the history
  • Loading branch information
atomrc committed Mar 26, 2019
1 parent 9773773 commit ec53cb3
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 8 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"sdp-transform": "2.7.0",
"simplebar": "3.1.3",
"speakingurl": "14.0.1",
"switch-path": "1.2.0",
"uint32": "0.2.1",
"underscore": "1.9.1",
"url-search-params-polyfill": "5.0.0",
Expand Down
4 changes: 2 additions & 2 deletions src/page/template/list/conversations.htm
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<!-- ko if: showCalls() -->
<!-- ko foreach: callConversations -->
<conversation-list-calling-cell
data-bind="click: $parent.clickOnConversation, attr: {'data-uie-uid': $data.id, 'data-uie-value': $data.display_name}"
data-bind="link_to: $parent.getConversationUrl($data), attr: {'data-uie-uid': $data.id, 'data-uie-value': $data.display_name}"
params="conversation: $data, callingRepository: $parent.callingRepository, permissionRepository: $parent.permissionRepository, multitasking: $parent.multitasking, videoGridRepository: $parent.videoGridRepository"
data-uie-name="item-call">
</conversation-list-calling-cell>
Expand Down Expand Up @@ -52,7 +52,7 @@

<!-- ko foreach: unarchivedConversations -->
<conversation-list-cell
data-bind="click: $parent.clickOnConversation, event: {'contextmenu': $parent.listViewModel.onContextMenu}"
data-bind="link_to: $parent.getConversationUrl($data), event: {'contextmenu': $parent.listViewModel.onContextMenu}"
data-uie-name="item-conversation"
params="click: $parent.listViewModel.onContextMenu, conversation: $data, is_selected: $parent.isSelectedConversation">
</conversation-list-cell>
Expand Down
11 changes: 10 additions & 1 deletion src/script/main/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import AppInitTelemetry from '../telemetry/app_init/AppInitTelemetry';
import {WindowHandler} from '../ui/WindowHandler';

import DebugUtil from '../util/DebugUtil';
import {Router} from '../router/Router';
import {initRouterBindings} from '../router/routerBindings';
import TimeUtil from 'utils/TimeUtil';

import '../components/mentionSuggestions.js';
Expand Down Expand Up @@ -603,17 +605,24 @@ class App {
*/
_showInterface() {
const conversationEntity = this.repository.conversation.getMostRecentConversation();

this.logger.info('Showing application UI');
if (this.repository.user.isTemporaryGuest()) {
this.view.list.showTemporaryGuest();
} else if (this.repository.user.shouldChangeUsername()) {
this.view.list.showTakeover();
} else if (conversationEntity) {
amplify.publish(z.event.WebApp.CONVERSATION.SHOW, conversationEntity);
this.view.content.showConversation(conversationEntity);
} else if (this.repository.user.connect_requests().length) {
amplify.publish(z.event.WebApp.CONTENT.SWITCH, z.viewModel.ContentViewModel.STATE.CONNECTION_REQUESTS);
}

const router = new Router({
'/conversation/:conversationId': conversationId => this.view.content.showConversation(conversationId),
'/user/:userId': () => {}, // TODO, implement showing the user profile modal
});
initRouterBindings(router);

this.view.loading.removeFromView();
$('#wire-main').attr('data-uie-value', 'is-loaded');

Expand Down
58 changes: 58 additions & 0 deletions src/script/router/Router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Wire
* Copyright (C) 2019 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import switchPath from 'switch-path';

export class Router {
constructor(routeDefinitions) {
const defaultRoute = {
// do nothing if url was not matched
'*': null,
};
const routes = Object.assign({}, defaultRoute, routeDefinitions);

const parseRoute = () => {
const currentPath = window.location.hash.replace('#', '') || '/';

const {value} = switchPath(currentPath, routes);
return typeof value === 'function' ? value() : value;
};

/**
* We need to proxy the replaceState method of history in order to trigger an event and warn the app that something happens.
* This is needed because the replaceState method can be called from outside of the app (eg. in the desktop app)
* @returns {void}
*/
const originalReplaceState = window.history.replaceState.bind(window.history);

window.history.replaceState = (...args) => {
originalReplaceState(...args);
parseRoute();
};
window.addEventListener('hashchange', parseRoute);

// tigger an initial parsing of the current url
parseRoute();
}

navigate(path) {
window.history.replaceState(null, null, `#${path}`);
return this;
}
}
34 changes: 34 additions & 0 deletions src/script/router/routerBindings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Wire
* Copyright (C) 2018 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import ko from 'knockout';

export function initRouterBindings(routerInstance) {
ko.bindingHandlers.link_to = {
init(element, valueAccessor) {
const navigate = event => {
routerInstance.navigate(valueAccessor());
event.preventDefault();
};
element.addEventListener('click', navigate);

ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('click', navigate));
},
};
}
7 changes: 2 additions & 5 deletions src/script/view_model/list/ConversationListViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ z.viewModel.list.ConversationListViewModel = class ConversationListViewModel {
* @param {Object} repositories - Object containing all repositories
*/
constructor(mainViewModel, listViewModel, repositories) {
this.clickOnConversation = this.clickOnConversation.bind(this);
this.isSelectedConversation = this.isSelectedConversation.bind(this);

this.callingRepository = repositories.calling;
Expand Down Expand Up @@ -129,10 +128,8 @@ z.viewModel.list.ConversationListViewModel = class ConversationListViewModel {
this.contentViewModel.switchContent(z.viewModel.ContentViewModel.STATE.CONNECTION_REQUESTS);
}

clickOnConversation(conversationEntity) {
if (!this.isSelectedConversation(conversationEntity)) {
this.contentViewModel.showConversation(conversationEntity);
}
getConversationUrl(conversationEntity) {
return `/conversation/${conversationEntity.id}`;
}

setShowCallsState(handlingNotifications) {
Expand Down
126 changes: 126 additions & 0 deletions test/unit_tests/router/RouterSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Wire
* Copyright (C) 2019 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {Router} from 'src/script/router/Router';

describe('Router', () => {
let originalReplaceStateFn;

beforeEach(() => {
originalReplaceStateFn = window.history.replaceState;
});

afterEach(() => {
window.location.hash = '#';
window.history.replaceState = originalReplaceStateFn;
});

describe('constructor', () => {
it("overwrites browser's replaceState method", () => {
const originalReplaceState = window.history.replaceState;
new Router();

expect(originalReplaceState).not.toBe(window.history.replaceState);
});

it('parse the current URL when instantiated', () => {
const routes = {'/conv': () => {}};
spyOn(routes, '/conv');

window.location.hash = '#/conv';
new Router(routes);

expect(routes['/conv']).toHaveBeenCalled();
});
});

describe('navigate', () => {
it('allows to navigate to specific url and call the associated handler', () => {
const handlers = {conversation: () => {}, user: () => {}};

spyOn(handlers, 'conversation');
spyOn(handlers, 'user');

const router = new Router({
'/conversation/:id': handlers.conversation,
'/user/:id': handlers.user,
});

router.navigate('/nomatch');

expect(handlers.conversation).not.toHaveBeenCalled();
expect(handlers.user).not.toHaveBeenCalled();

router.navigate('/conversation/uuid');

expect(handlers.conversation).toHaveBeenCalled();
expect(handlers.user).not.toHaveBeenCalled();

router.navigate('/user/uuid');

expect(handlers.user).toHaveBeenCalled();
});
});

describe('hash change event listener', () => {
it('triggers routing when a hashchange event is triggered', done => {
const handlers = {conversation: () => {}};
spyOn(handlers, 'conversation');

new Router({'/conversation/:id': handlers.conversation});

expect(handlers.conversation).not.toHaveBeenCalled();

window.location.hash = '#/conversation/uuid';

setTimeout(() => {
expect(handlers.conversation).toHaveBeenCalled();
done();
});
});
});

describe('history.replaceState proxy', () => {
it('calls the matching handler when a new state is replaced', () => {
const routes = {
'/conversation/:id': () => {},
'/user/:id': () => {},
};

spyOn(routes, '/conversation/:id');
spyOn(routes, '/user/:id');

new Router(routes);

window.history.replaceState('', '', '#/nomatch');

expect(routes['/conversation/:id']).not.toHaveBeenCalled();
expect(routes['/user/:id']).not.toHaveBeenCalled();

window.history.replaceState('', '', '#/conversation/uuid');

expect(routes['/conversation/:id']).toHaveBeenCalled();
expect(routes['/user/:id']).not.toHaveBeenCalled();

window.history.replaceState('', '', '#/user/uuid');

expect(routes['/user/:id']).toHaveBeenCalled();
});
});
});
38 changes: 38 additions & 0 deletions test/unit_tests/router/routerBindingsSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Wire
* Copyright (C) 2019 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {initRouterBindings} from 'src/script/router/routerBindings';
import {bindHtml} from '../../api/knockoutHelpers';

describe('routerBindings', () => {
let mockRouter;
beforeEach(() => {
mockRouter = {navigate: () => {}};
initRouterBindings(mockRouter);
spyOn(mockRouter, 'navigate');
});

it('handles click and triggers router navigation', async () => {
const url = '/conversation/uuid';
const domElement = await bindHtml(`<a data-bind="link_to: '${url}'">click me</a>`);
domElement.querySelector('a').click();

expect(mockRouter.navigate).toHaveBeenCalledWith(url);
});
});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11498,6 +11498,11 @@ svgo@^1.0.0:
unquote "~1.1.1"
util.promisify "~1.0.0"

switch-path@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/switch-path/-/switch-path-1.2.0.tgz#3f872cb3c7eb2b77fb9509dea4e3f3ff25ecf556"
integrity sha1-P4css8frK3f7lQnepOPz/yXs9VY=

symbol-observable@^1.1.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
Expand Down

0 comments on commit ec53cb3

Please sign in to comment.