tag', () => {
8 | const renderedComponent = shallow(
);
9 | expect(renderedComponent.type()).toEqual('div');
10 | });
11 |
12 | it('should have a className attribute', () => {
13 | const renderedComponent = shallow(
);
14 | expect(renderedComponent.prop('className')).toBeDefined();
15 | });
16 |
17 | it('should adopt a valid attribute', () => {
18 | const id = 'test';
19 | const renderedComponent = shallow(
);
20 | expect(renderedComponent.prop('id')).toEqual(id);
21 | });
22 |
23 | it('should not adopt an invalid attribute', () => {
24 | const renderedComponent = shallow(
);
25 | expect(renderedComponent.prop('attribute')).toBeUndefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/app/containers/LocaleToggle/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { browserHistory } from 'react-router-dom';
4 | import { shallow, mount } from 'enzyme';
5 |
6 | import LocaleToggle, { mapDispatchToProps } from '../index';
7 | import { changeLocale } from '../../LanguageProvider/actions';
8 | import LanguageProvider from '../../LanguageProvider';
9 |
10 | import configureStore from '../../../configureStore';
11 | import { translationMessages } from '../../../i18n';
12 |
13 | describe('
', () => {
14 | let store;
15 |
16 | beforeAll(() => {
17 | store = configureStore({}, browserHistory);
18 | });
19 |
20 | it('should render the default language messages', () => {
21 | const renderedComponent = shallow(
22 |
23 |
24 |
25 |
26 |
27 | );
28 | expect(renderedComponent.contains(
)).toBe(true);
29 | });
30 |
31 | it('should present the default `en` english language option', () => {
32 | const renderedComponent = mount(
33 |
34 |
35 |
36 |
37 |
38 | );
39 | expect(renderedComponent.contains(
)).toBe(true);
40 | });
41 |
42 | describe('mapDispatchToProps', () => {
43 | describe('onLocaleToggle', () => {
44 | it('should be injected', () => {
45 | const dispatch = jest.fn();
46 | const result = mapDispatchToProps(dispatch);
47 | expect(result.onLocaleToggle).toBeDefined();
48 | });
49 |
50 | it('should dispatch changeLocale when called', () => {
51 | const dispatch = jest.fn();
52 | const result = mapDispatchToProps(dispatch);
53 | const locale = 'de';
54 | const evt = { target: { value: locale } };
55 | result.onLocaleToggle(evt);
56 | expect(dispatch).toHaveBeenCalledWith(changeLocale(locale));
57 | });
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/app/containers/MessageList/Loadable.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Asynchronously loads the component for MessageList
4 | *
5 | */
6 |
7 | import Loadable from 'react-loadable';
8 |
9 | export default Loadable({
10 | loader: () => import('./index'),
11 | loading: () => null,
12 | });
13 |
--------------------------------------------------------------------------------
/app/containers/MessageList/actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * MessageList actions
4 | *
5 | */
6 |
7 | import {
8 | RESET_CONTACT_SEARCH,
9 | FETCH_PROFILES,
10 | LOAD_PROFILES_SUCCESS,
11 | LOAD_PROFILES_ERROR,
12 | CHANGE_USERNAME,
13 | } from './constants';
14 |
15 | import {
16 | GET_PUBLIC_PROFILE,
17 | } from '../ContactList/constants';
18 |
19 | export function getPublicProfile(contact) {
20 | return {
21 | type: GET_PUBLIC_PROFILE,
22 | contact,
23 | };
24 | }
25 |
26 | export function changeUsername(name) {
27 | return {
28 | type: CHANGE_USERNAME,
29 | name,
30 | };
31 | }
32 |
33 | export function resetContactSearch() {
34 | return {
35 | type: RESET_CONTACT_SEARCH,
36 | };
37 | }
38 |
39 | export function fetchProfiles() {
40 | return {
41 | type: FETCH_PROFILES,
42 | };
43 | }
44 |
45 | export function profilesLoaded(profiles) {
46 | return {
47 | type: LOAD_PROFILES_SUCCESS,
48 | profiles,
49 | };
50 | }
51 |
52 | export function profileLoadingError(error) {
53 | return {
54 | type: LOAD_PROFILES_ERROR,
55 | error,
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/app/containers/MessageList/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * MessageList constants
4 | *
5 | */
6 |
7 | export const LOAD_PROFILES_SUCCESS = 'app/MessageList/LOAD_PROFILES_SUCCESS';
8 | export const LOAD_PROFILES_ERROR = 'app/MessageList/LOAD_PROFILES_ERROR';
9 | export const STORE_MESSAGES = 'app/MessageList/STORE_MESSAGES';
10 | export const SET_CONTACT_SEARCH = 'app/MessageList/SET_CONTACT_SEARCH';
11 | export const FETCH_PROFILES = 'app/MessageList/FETCH_PROFILES';
12 | export const CHANGE_USERNAME = 'app/MessageList/CHANGE_USERNAME';
13 | export const RESET_CONTACT_SEARCH = 'app/MessageList/RESET_CONTACT_SEARCH';
14 |
--------------------------------------------------------------------------------
/app/containers/MessageList/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * MessageList reducer
4 | *
5 | */
6 |
7 | import update from 'immutability-helper';
8 |
9 | import {
10 | RESET_CONTACT_SEARCH,
11 | FETCH_PROFILES,
12 | CHANGE_USERNAME,
13 | LOAD_PROFILES_SUCCESS,
14 | LOAD_PROFILES_ERROR,
15 | STORE_MESSAGES,
16 | SET_CONTACT_SEARCH,
17 | } from './constants';
18 |
19 | // The initial state of the App
20 | const initialState = {
21 | username: '',
22 | loading: false,
23 | error: '',
24 | profiles: [],
25 | messages: [],
26 | newContactSearch: false,
27 | };
28 |
29 | function messageListReducer(state = initialState, action) {
30 | switch (action.type) {
31 | case RESET_CONTACT_SEARCH:
32 | return update(state, {
33 | username: { $set: '' },
34 | loading: { $set: false },
35 | error: { $set: '' },
36 | profiles: { $set: [] },
37 | messages: { $set: [] },
38 | });
39 | case CHANGE_USERNAME:
40 | return update(state, {
41 | username: { $set: action.name },
42 | });
43 | case FETCH_PROFILES:
44 | return update(state, {
45 | loading: { $set: true },
46 | error: { $set: '' },
47 | profiles: { $set: [] },
48 | });
49 | case LOAD_PROFILES_SUCCESS:
50 | return update(state, {
51 | profiles: { $set: action.profiles },
52 | loading: { $set: false },
53 | });
54 | case LOAD_PROFILES_ERROR:
55 | return update(state, {
56 | error: { $set: action.error },
57 | loading: { $set: false },
58 | });
59 | case STORE_MESSAGES:
60 | return update(state, {
61 | messages: { $set: action.messages },
62 | });
63 | case SET_CONTACT_SEARCH:
64 | return update(state, {
65 | newContactSearch: { $set: action.newContactSearch },
66 | });
67 | default:
68 | return state;
69 | }
70 | }
71 |
72 | export default messageListReducer;
73 |
--------------------------------------------------------------------------------
/app/containers/MessageList/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const selectMessageList = (state) => state.messageList;
4 |
5 | const makeSelectMessageList = () => createSelector(
6 | selectMessageList,
7 | (messageListState) => messageListState
8 | );
9 |
10 | const makeSelectUsername = () => createSelector(
11 | selectMessageList,
12 | (messageListState) => messageListState.username
13 | );
14 |
15 | const makeSelectLoading = () => createSelector(
16 | selectMessageList,
17 | (messageListState) => messageListState.loading
18 | );
19 |
20 | const makeSelectError = () => createSelector(
21 | selectMessageList,
22 | (messageListState) => messageListState.error
23 | );
24 |
25 | const makeSelectMessages = () => createSelector(
26 | selectMessageList,
27 | (messageListState) => messageListState.messages
28 | );
29 |
30 | const makeSelectNewContactSearch = () => createSelector(
31 | selectMessageList,
32 | (messageListState) => messageListState.newContactSearch
33 | );
34 |
35 | export {
36 | selectMessageList,
37 | makeSelectMessages,
38 | makeSelectMessageList,
39 | makeSelectNewContactSearch,
40 | makeSelectUsername,
41 | makeSelectLoading,
42 | makeSelectError,
43 | };
44 |
--------------------------------------------------------------------------------
/app/containers/MessageList/tests/actions.test.js:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | defaultAction,
4 | } from '../actions';
5 | import {
6 | DEFAULT_ACTION,
7 | } from '../constants';
8 |
9 | describe('MessageList actions', () => {
10 | describe('Default Action', () => {
11 | it('has a type of DEFAULT_ACTION', () => {
12 | const expected = {
13 | type: DEFAULT_ACTION,
14 | };
15 | expect(defaultAction()).toEqual(expected);
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/app/containers/MessageList/tests/index.test.js:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { shallow } from 'enzyme';
3 |
4 | // import { MessageList } from '../index';
5 |
6 | describe('
', () => {
7 | it('Expect to have unit tests specified', () => {
8 | // expect(true).toEqual(false);
9 | expect(false).toEqual(false);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/app/containers/MessageList/tests/reducer.test.js:
--------------------------------------------------------------------------------
1 |
2 | // import { fromJS } from 'immutable';
3 | import messageListReducer from '../reducer';
4 |
5 | describe('messageListReducer', () => {
6 | it('returns the initial state', () => {
7 | expect(messageListReducer(undefined, {})).toEqual({});
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/app/containers/MessageList/tests/saga.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test sagas
3 | */
4 |
5 | /* eslint-disable redux-saga/yield-effects */
6 | // import { take, call, put, select } from 'redux-saga/effects';
7 | // import { defaultSaga } from '../saga';
8 |
9 | // const generator = defaultSaga();
10 |
11 | describe('defaultSaga Saga', () => {
12 | it('Expect to have unit tests specified', () => {
13 | // expect(true).toEqual(false);
14 | expect(false).toEqual(false);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/app/containers/MessageList/tests/selectors.test.js:
--------------------------------------------------------------------------------
1 | // import { fromJS } from 'immutable';
2 | // import { selectMessageListDomain } from '../selectors';
3 |
4 | describe('selectMessageListDomain', () => {
5 | it('Expect to have unit tests specified', () => {
6 | // expect(true).toEqual(false);
7 | expect(false).toEqual(false);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/app/containers/MessageList/workers.js:
--------------------------------------------------------------------------------
1 | export function handleResultSelect(e, { result }) {
2 | this.setState({ value: '' });
3 | this.props.handleSearchSelect(result);
4 | this.props.resetContactSearch();
5 | }
6 |
7 | export function handleFormSubmit(event) {
8 | event.preventDefault();
9 | if (this.props.slowSearch) {
10 | setTimeout(() => {
11 | this.props.changeUsername(this.state.value);
12 | this.props.fetchProfiles();
13 | }, 500);
14 | }
15 | }
16 |
17 | export function handleSearchChange(e, { value }) {
18 | this.setState({ value });
19 | if (value.length < 1) return this.props.resetContactSearch();
20 | const timeout = (value.length < 3) ? 1000 : 500
21 | if (!this.props.slowSearch) {
22 | setTimeout(() => {
23 | this.props.changeUsername(this.state.value);
24 | this.props.fetchProfiles();
25 | }, timeout);
26 | }
27 | }
28 |
29 | export function getBase64(file) {
30 | return new Promise((resolve, reject) => {
31 | const reader = new FileReader();
32 | reader.readAsDataURL(file);
33 | reader.onload = () => resolve(reader.result);
34 | reader.onerror = (error) => reject(error);
35 | });
36 | }
37 |
38 | export function onDrop(files) {
39 | if (this.state.status !== 'green') {
40 | this.setState({ userOfflineError: true });
41 | } else {
42 | const pako = require('pako');
43 | this.props.logger('files uploading', files);
44 | this.getBase64(files[0]).then(
45 | (data) => {
46 | this.props.logger(data);
47 | this.props.logger(pako.deflate(data));
48 | }
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/Loadable.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Asynchronously loads the component for MessagePage
4 | *
5 | */
6 |
7 | import Loadable from 'react-loadable';
8 |
9 | export default Loadable({
10 | loader: () => import('./index'),
11 | loading: () => null,
12 | });
13 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * MessagePage actions
4 | *
5 | */
6 |
7 | import {
8 | STORE_CONTACT_MANAGER,
9 | CHANGE_ADD_USERNAME,
10 | ADD_FETCH_PROFILES,
11 | ADD_PROFILE_SUCCESS,
12 | ADD_PROFILE_ERROR,
13 | SET_SHOW_ADD,
14 | SET_MESSAGE_SCROLL_TOP,
15 | ADD_DISCOVERY_CONTACT,
16 | DEFAULT_PROFILE_ADD,
17 | } from './constants';
18 |
19 | import {
20 | SET_CONTACT_SEARCH,
21 | STORE_MESSAGES,
22 | } from '../MessageList/constants';
23 |
24 |
25 |
26 | export function defaultProfileAdd(defaultAddName) {
27 | return {
28 | type: DEFAULT_PROFILE_ADD,
29 | defaultAddName
30 | };
31 | }
32 |
33 | export function setAddContactName(addName) {
34 | return {
35 | type: CHANGE_ADD_USERNAME,
36 | addName,
37 | };
38 | }
39 |
40 | export function addFetchProfiles() {
41 | return {
42 | type: ADD_FETCH_PROFILES,
43 | };
44 | }
45 |
46 | export function storeContactMgr(contactMgr) {
47 | return {
48 | type: STORE_CONTACT_MANAGER,
49 | contactMgr,
50 | };
51 | }
52 |
53 | export function storeMessages(messages) {
54 | return {
55 | type: STORE_MESSAGES,
56 | messages,
57 | };
58 | }
59 |
60 | export function setContactSearch(newContactSearch) {
61 | return {
62 | type: SET_CONTACT_SEARCH,
63 | newContactSearch,
64 | };
65 | }
66 |
67 | export function addProfileLoaded(addProfile, showAdd, defaultAdd) {
68 | return {
69 | type: ADD_PROFILE_SUCCESS,
70 | addProfile,
71 | showAdd,
72 | defaultAdd
73 | };
74 | }
75 |
76 | export function addProfileLoadingError(error) {
77 | return {
78 | type: ADD_PROFILE_ERROR,
79 | error,
80 | };
81 | }
82 |
83 | export function setShowAdd(showAdd) {
84 | return {
85 | type: SET_SHOW_ADD,
86 | showAdd,
87 | };
88 | }
89 |
90 | export function setMsgScrollTop(flag) {
91 | return {
92 | type: SET_MESSAGE_SCROLL_TOP,
93 | flag,
94 | };
95 | }
96 |
97 | export function addDiscoveryContact(id, path) {
98 | return {
99 | type: ADD_DISCOVERY_CONTACT,
100 | id,
101 | path,
102 | };
103 | }
104 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * MessagePage constants
4 | *
5 | */
6 |
7 | export const STORE_CONTACT_MANAGER = 'app/MessagePage/STORE_CONTACT_MANAGER';
8 | export const CHANGE_ADD_USERNAME = 'app/MessagePage/CHANGE_ADD_USERNAME';
9 | export const ADD_FETCH_PROFILES = 'app/MessagePage/ADD_FETCH_PROFILES';
10 | export const ADD_PROFILE_SUCCESS = 'app/MessagePage/ADD_PROFILE_SUCCESS';
11 | export const ADD_PROFILE_ERROR = 'app/MessagePage/ADD_PROFILE_ERROR';
12 | export const SET_SHOW_ADD = 'app/MessagePage/SET_SHOW_ADD';
13 | export const SET_MESSAGE_SCROLL_TOP = 'app/MessagePage/SET_MESSAGE_SCROLL_TOP';
14 | export const ADD_DISCOVERY_CONTACT = 'app/MessagePage/ADD_DISCOVERY_CONTACT';
15 | export const DEFAULT_PROFILE_ADD = 'app/MessagePage/DEFAULT_PROFILE_ADD';
16 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * MessagePage reducer
4 | *
5 | */
6 |
7 | import update from 'immutability-helper';
8 | const { ContactManager } = require('./../../contactManager.js');
9 |
10 | import {
11 | STORE_CONTACT_MANAGER,
12 | CHANGE_ADD_USERNAME,
13 | ADD_PROFILE_SUCCESS,
14 | ADD_PROFILE_ERROR,
15 | SET_SHOW_ADD,
16 | DEFAULT_PROFILE_ADD,
17 | SET_MESSAGE_SCROLL_TOP,
18 | } from './constants';
19 |
20 | const initialState = {
21 | contactMgr: new ContactManager(),
22 | addName: '',
23 | defaultAddName: '',
24 | addProfile: {},
25 | error: '',
26 | showAdd: false,
27 | defaultAdd: false,
28 | msgScrollTop: false,
29 | };
30 |
31 | function messagePageReducer(state = initialState, action) {
32 | switch (action.type) {
33 | case DEFAULT_PROFILE_ADD:
34 | return update(state, {
35 | defaultAddName: { $set: action.defaultAddName },
36 | });
37 | case STORE_CONTACT_MANAGER:
38 | return update(state, {
39 | contactMgr: { $set: action.contactMgr },
40 | });
41 | case CHANGE_ADD_USERNAME:
42 | return update(state, {
43 | addName: { $set: action.addName },
44 | });
45 | case ADD_PROFILE_SUCCESS:
46 | return update(state, {
47 | addProfile: { $set: action.addProfile },
48 | showAdd: { $set: action.showAdd },
49 | defaultAdd: { $set: action.defaultAdd },
50 | error: { $set: '' },
51 | });
52 | case ADD_PROFILE_ERROR:
53 | return update(state, {
54 | addProfile: { $set: {} },
55 | error: { $set: action.error },
56 | });
57 | case SET_SHOW_ADD:
58 | return update(state, {
59 | showAdd: { $set: action.showAdd },
60 | });
61 | case SET_MESSAGE_SCROLL_TOP:
62 | return update(state, {
63 | msgScrollTop: { $set: action.flag },
64 | });
65 | default:
66 | return state;
67 | }
68 | }
69 |
70 | export default messagePageReducer;
71 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const selectMessagePage = (state) => state.messagePage;
4 |
5 | const makeSelectMessagePage = () => createSelector(
6 | selectMessagePage,
7 | (messagePageState) => messagePageState
8 | );
9 |
10 | const makeSelectContactMgr = () => createSelector(
11 | selectMessagePage,
12 | (messagePageState) => messagePageState.contactMgr
13 | );
14 |
15 | const makeSelectAddName = () => createSelector(
16 | selectMessagePage,
17 | (messagePageState) => messagePageState.addName
18 | );
19 |
20 | const makeSelectDefaultAddName = () => createSelector(
21 | selectMessagePage,
22 | (messagePageState) => messagePageState.defaultAddName
23 | );
24 |
25 | const makeSelectAddProfile = () => createSelector(
26 | selectMessagePage,
27 | (messagePageState) => messagePageState.addProfile
28 | );
29 |
30 | const makeSelectShowAdd = () => createSelector(
31 | selectMessagePage,
32 | (messagePageState) => messagePageState.showAdd
33 | );
34 |
35 | const makeSelectDefaultAdd = () => createSelector(
36 | selectMessagePage,
37 | (messagePageState) => messagePageState.defaultAdd
38 | );
39 |
40 | const makeSelectScrollTop = () => createSelector(
41 | selectMessagePage,
42 | (messagePageState) => messagePageState.msgScrollTop
43 | );
44 |
45 | export {
46 | selectMessagePage,
47 | makeSelectShowAdd,
48 | makeSelectDefaultAdd,
49 | makeSelectAddName,
50 | makeSelectDefaultAddName,
51 | makeSelectAddProfile,
52 | makeSelectContactMgr,
53 | makeSelectMessagePage,
54 | makeSelectScrollTop,
55 | };
56 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/tests/actions.test.js:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | defaultAction,
4 | } from '../actions';
5 | import {
6 | DEFAULT_ACTION,
7 | } from '../constants';
8 |
9 | describe('MessagePage actions', () => {
10 | describe('Default Action', () => {
11 | it('has a type of DEFAULT_ACTION', () => {
12 | const expected = {
13 | type: DEFAULT_ACTION,
14 | };
15 | expect(defaultAction()).toEqual(expected);
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/tests/index.test.js:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { shallow } from 'enzyme';
3 |
4 | // import { MessagePage } from '../index';
5 |
6 | describe('
', () => {
7 | it('Expect to have unit tests specified', () => {
8 | expect(true).toEqual(false);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/tests/reducer.test.js:
--------------------------------------------------------------------------------
1 |
2 | import { fromJS } from 'immutable';
3 | import messagePageReducer from '../reducer';
4 |
5 | describe('messagePageReducer', () => {
6 | it('returns the initial state', () => {
7 | expect(messagePageReducer(undefined, {})).toEqual(fromJS({}));
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/tests/saga.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test sagas
3 | */
4 |
5 | /* eslint-disable redux-saga/yield-effects */
6 | // import { take, call, put, select } from 'redux-saga/effects';
7 | // import { defaultSaga } from '../saga';
8 |
9 | // const generator = defaultSaga();
10 |
11 | describe('defaultSaga Saga', () => {
12 | it('Expect to have unit tests specified', () => {
13 | expect(true).toEqual(false);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/app/containers/MessagePage/tests/selectors.test.js:
--------------------------------------------------------------------------------
1 | // import { fromJS } from 'immutable';
2 | // import { selectMessagePageDomain } from '../selectors';
3 |
4 | describe('selectMessagePageDomain', () => {
5 | it('Expect to have unit tests specified', () => {
6 | expect(true).toEqual(false);
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/app/containers/NotFoundPage/Loadable.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Asynchronously loads the component for NotFoundPage
3 | */
4 | import Loadable from 'react-loadable';
5 |
6 | import LoadingIndicator from 'components/LoadingIndicator';
7 |
8 | export default Loadable({
9 | loader: () => import('./index'),
10 | loading: LoadingIndicator,
11 | });
12 |
--------------------------------------------------------------------------------
/app/containers/NotFoundPage/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NotFoundPage
3 | *
4 | * This is the page we show when the user visits a url that doesn't have a route
5 | */
6 |
7 | import React from 'react';
8 | import { FormattedMessage } from 'react-intl';
9 |
10 | import H1 from 'components/H1';
11 | import messages from './messages';
12 |
13 | export default function NotFound() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/app/containers/NotFoundPage/messages.js:
--------------------------------------------------------------------------------
1 | /*
2 | * NotFoundPage Messages
3 | *
4 | * This contains all the text for the NotFoundPage component.
5 | */
6 | import { defineMessages } from 'react-intl';
7 |
8 | export default defineMessages({
9 | header: {
10 | id: 'boilerplate.containers.NotFoundPage.header',
11 | defaultMessage: 'Page not found.',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/app/containers/NotFoundPage/tests/index.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Testing the NotFoundPage
3 | */
4 |
5 | import React from 'react';
6 | import { shallow } from 'enzyme';
7 | import { FormattedMessage } from 'react-intl';
8 |
9 | import H1 from 'components/H1';
10 | import NotFound from '../index';
11 |
12 | describe('
', () => {
13 | it('should render the Page Not Found text', () => {
14 | const renderedComponent = shallow(
15 |
16 | );
17 | expect(renderedComponent.contains(
18 |
19 |
23 |
)).toEqual(true);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/Loadable.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Asynchronously loads the component for ToolBar
4 | *
5 | */
6 |
7 | import Loadable from 'react-loadable';
8 |
9 | export default Loadable({
10 | loader: () => import('./index'),
11 | loading: () => null,
12 | });
13 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * ToolBar actions
4 | *
5 | */
6 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * ToolBar constants
4 | *
5 | */
6 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * ToolBar reducer
4 | *
5 | */
6 |
7 | import {
8 | } from './constants';
9 |
10 | const initialState = {};
11 |
12 | function toolBarReducer(state = initialState, action) {
13 | switch (action.type) {
14 | default:
15 | return state;
16 | }
17 | }
18 |
19 | export default toolBarReducer;
20 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/saga.js:
--------------------------------------------------------------------------------
1 | // import { take, call, put, select } from 'redux-saga/effects';
2 |
3 | // Individual exports for testing
4 | export default function* defaultSaga() {
5 | // See example in containers/HomePage/saga.js
6 | }
7 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const selectToolBar = (state) => state.toolBar;
4 |
5 | const makeSelectToolBar = () => createSelector(
6 | selectToolBar,
7 | (toolBarState) => toolBarState
8 | );
9 |
10 | export default makeSelectToolBar;
11 | export {
12 | selectToolBar,
13 | };
14 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/tests/actions.test.js:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | defaultAction,
4 | } from '../actions';
5 | import {
6 | DEFAULT_ACTION,
7 | } from '../constants';
8 |
9 | describe('ToolBar actions', () => {
10 | describe('Default Action', () => {
11 | it('has a type of DEFAULT_ACTION', () => {
12 | const expected = {
13 | type: DEFAULT_ACTION,
14 | };
15 | expect(defaultAction()).toEqual(expected);
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/tests/index.test.js:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { shallow } from 'enzyme';
3 |
4 | // import { ToolBar } from '../index';
5 |
6 | describe('
', () => {
7 | it('Expect to have unit tests specified', () => {
8 | // expect(true).toEqual(false);
9 | expect(false).toEqual(false);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/tests/reducer.test.js:
--------------------------------------------------------------------------------
1 |
2 | import { fromJS } from 'immutable';
3 | import toolBarReducer from '../reducer';
4 |
5 | describe('toolBarReducer', () => {
6 | it('returns the initial state', () => {
7 | expect(toolBarReducer(undefined, {})).toEqual(fromJS({}));
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/tests/saga.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test sagas
3 | */
4 |
5 | /* eslint-disable redux-saga/yield-effects */
6 | // import { take, call, put, select } from 'redux-saga/effects';
7 | // import { defaultSaga } from '../saga';
8 |
9 | // const generator = defaultSaga();
10 |
11 | describe('defaultSaga Saga', () => {
12 | it('Expect to have unit tests specified', () => {
13 | // expect(true).toEqual(false);
14 | expect(false).toEqual(false);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/tests/selectors.test.js:
--------------------------------------------------------------------------------
1 | // import { fromJS } from 'immutable';
2 | // import { selectToolBarDomain } from '../selectors';
3 |
4 | describe('selectToolBarDomain', () => {
5 | it('Expect to have unit tests specified', () => {
6 | // expect(true).toEqual(false);
7 | expect(false).toEqual(false);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/app/containers/ToolBar/workers.js:
--------------------------------------------------------------------------------
1 | export function updateDynamicStep() {
2 | let dynamicStep = {};
3 | if (this.props.newContactSearch || !this.props.contactMgr.getActiveContact()) {
4 | dynamicStep = {
5 | title: 'Search Bar',
6 | text: 'Search for a blockstack user\'s name or id',
7 | selector: '.search',
8 | position: 'bottom',
9 | style: {
10 | mainColor: '#f07b50',
11 | beacon: {
12 | inner: '#f07b50',
13 | outer: '#f07b50',
14 | },
15 | },
16 | };
17 | } else {
18 | dynamicStep = {
19 | title: 'Active Contact',
20 | text: 'Here you can see contact\'s profile, remove them, or video chat',
21 | selector: '.activeContact',
22 | position: 'top-right',
23 | style: {
24 | mainColor: '#a350f0',
25 | beacon: {
26 | inner: '#a350f0',
27 | outer: '#a350f0',
28 | },
29 | },
30 | };
31 | }
32 | this.props.updateSteps(dynamicStep);
33 | this.toggleVisibility();
34 | this.props.startTour();
35 | }
36 |
37 | export function handleItemClick(e, { name }) {
38 | if (name === 'settings') {
39 | this.props.handleSettingsOpen();
40 | this.toggleVisibility();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/Loadable.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Asynchronously loads the component for UserIdForm
4 | *
5 | */
6 |
7 | import Loadable from 'react-loadable';
8 |
9 | export default Loadable({
10 | loader: () => import('./index'),
11 | loading: () => null,
12 | });
13 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * UserIdForm actions
4 | *
5 | */
6 |
7 | import {
8 | DEFAULT_ACTION,
9 | } from './constants';
10 |
11 | export function defaultAction() {
12 | return {
13 | type: DEFAULT_ACTION,
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * UserIdForm constants
4 | *
5 | */
6 |
7 | export const DEFAULT_ACTION = 'app/UserIdForm/DEFAULT_ACTION';
8 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * UserIdForm reducer
4 | *
5 | */
6 |
7 | import {
8 | DEFAULT_ACTION,
9 | } from './constants';
10 |
11 | const initialState = {};
12 |
13 | function userIdFormReducer(state = initialState, action) {
14 | switch (action.type) {
15 | case DEFAULT_ACTION:
16 | return state;
17 | default:
18 | return state;
19 | }
20 | }
21 |
22 | export default userIdFormReducer;
23 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/saga.js:
--------------------------------------------------------------------------------
1 | // import { take, call, put, select } from 'redux-saga/effects';
2 |
3 | // Individual exports for testing
4 | export default function* defaultSaga() {
5 | // See example in containers/HomePage/saga.js
6 | }
7 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/selectors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Direct selector to the userIdForm state domain
3 | */
4 | export const selectUserIdFormDomain = (state) => state.userIdForm;
5 |
6 | /**
7 | * Other specific selectors
8 | */
9 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/tests/actions.test.js:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | defaultAction,
4 | } from '../actions';
5 | import {
6 | DEFAULT_ACTION,
7 | } from '../constants';
8 |
9 | describe('UserIdForm actions', () => {
10 | describe('Default Action', () => {
11 | it('has a type of DEFAULT_ACTION', () => {
12 | const expected = {
13 | type: DEFAULT_ACTION,
14 | };
15 | expect(defaultAction()).toEqual(expected);
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/tests/index.test.js:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { shallow } from 'enzyme';
3 |
4 | // import { UserIdForm } from '../index';
5 |
6 | describe('
', () => {
7 | it('Expect to have unit tests specified', () => {
8 | expect(true).toEqual(false);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/tests/reducer.test.js:
--------------------------------------------------------------------------------
1 | import userIdFormReducer from '../reducer';
2 |
3 | describe('userIdFormReducer', () => {
4 | it('returns the initial state', () => {
5 | expect(userIdFormReducer(undefined, {})).toEqual({});
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/tests/saga.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test sagas
3 | */
4 |
5 | /* eslint-disable redux-saga/yield-effects */
6 | // import { take, call, put, select } from 'redux-saga/effects';
7 | // import { defaultSaga } from '../saga';
8 |
9 | // const generator = defaultSaga();
10 |
11 | describe('defaultSaga Saga', () => {
12 | it('Expect to have unit tests specified', () => {
13 | expect(true).toEqual(false);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/app/containers/UserIdForm/tests/selectors.test.js:
--------------------------------------------------------------------------------
1 | // import { selectUserIdFormDomain } from '../selectors';
2 |
3 | describe('selectUserIdFormDomain', () => {
4 | it('Expect to have unit tests specified', () => {
5 | expect(true).toEqual(false);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/app/filesystem/baseIO.js:
--------------------------------------------------------------------------------
1 | // Abstract base class for IO services.
2 | //
3 | // Pattern from: https://ilikekillnerds.com/2015/06/abstract-classes-in-javascript/
4 | //
5 | module.exports = class BaseIO {
6 | constructor() {
7 | if (this.constructor === BaseIO) {
8 | throw new TypeError('Abstract class "IO" cannot be instantiated directly.');
9 | }
10 |
11 | if (this.writeLocalFile === undefined) {
12 | throw new TypeError('Classes extending the BaseIO abstract class must implement: ' +
13 | 'writeLocalFile(localUser, fileName, data)');
14 | }
15 |
16 | if (this.readLocalFile === undefined) {
17 | throw new TypeError('Classes extending the BaseIO abstract class must implement: ' +
18 | 'readLocalFile(localUser, fileName)');
19 | }
20 |
21 | if (this.deleteLocalFile === undefined) {
22 | throw new TypeError('Classes extending the BaseIO abstract class must implement: ' +
23 | 'deleteLocalFile(localUser, filename)');
24 | }
25 |
26 | if (this.readRemoteFile === undefined) {
27 | throw new TypeError('Classes extending the BaseIO abstract class must implement: ' +
28 | 'readRemoteFile(remoteUser, fileName)');
29 | }
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/app/filesystem/firebaseIO.js:
--------------------------------------------------------------------------------
1 | const BaseIO = require('./baseIO.js');
2 |
3 | const ROOT = '/global/gaia';
4 | const APP_NAME = 'stealthy.im';
5 |
6 | module.exports = class FirebaseIO extends BaseIO {
7 | constructor(logger, firebaseInst, pathURL) {
8 | super();
9 | this.logger = logger;
10 | this.firebaseInst = firebaseInst;
11 | this.pathURL = pathURL;
12 | }
13 |
14 | // Public:
15 | //
16 | writeLocalFile(localUser, fileName, data) {
17 | const filePath = `${this._getLocalApplicationPath(localUser)}/${fileName}`;
18 | return this._write(filePath, data);
19 | }
20 |
21 | readLocalFile(localUser, fileName) {
22 | const filePath = `${this._getLocalApplicationPath(localUser)}/${fileName}`;
23 | return this._read(filePath);
24 | }
25 |
26 | deleteLocalFile(localUser, fileName) {
27 | const filePath = `${this._getLocalApplicationPath(localUser)}/${fileName}`;
28 | return this._delete(filePath);
29 | }
30 |
31 | readRemoteFile(remoteUser, fileName) {
32 | const filePath = `${this._getRemoteApplicationPath(remoteUser)}/${fileName}`;
33 | return this._read(filePath);
34 | }
35 |
36 | // Private:
37 | //
38 | _cleanPathForFirebase(path) {
39 | return path.replace(/\./g, '_');
40 | }
41 |
42 | _getLocalApplicationPath(localUser, appName = APP_NAME) {
43 | return `${ROOT}/${localUser}/${this.pathURL}/${APP_NAME}`;
44 | }
45 |
46 | _getRemoteApplicationPath(remoteUser, appName = APP_NAME) {
47 | return `${ROOT}/${remoteUser}/${this.pathURL}/${APP_NAME}`;
48 | }
49 |
50 | _write(filePath, data) {
51 | const cleanPath = this._cleanPathForFirebase(filePath);
52 | this.logger(`Writing data to: ${cleanPath}`);
53 | return this.firebaseInst.database().ref(cleanPath).set(data);
54 | }
55 |
56 | _read(filePath) {
57 | const cleanPath = this._cleanPathForFirebase(filePath);
58 | const targetRef = this.firebaseInst.database().ref(cleanPath);
59 |
60 | return targetRef.once('value')
61 | .then((snapshot) => {
62 | this.logger(`Read data from: ${cleanPath}`);
63 | return snapshot.val();
64 | })
65 | .catch((error) => {
66 | this.logger(`Read failed from: ${cleanPath}`);
67 | return undefined;
68 | });
69 | }
70 |
71 | _delete(filePath) {
72 | const cleanPath = this._cleanPathForFirebase(filePath);
73 | this.logger(`Deleting ${cleanPath}`);
74 | return this.firebaseInst.database().ref(cleanPath).remove();
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/app/global-styles.js:
--------------------------------------------------------------------------------
1 | import { injectGlobal } from 'styled-components';
2 |
3 | /* eslint no-unused-expressions: 0 */
4 | injectGlobal`
5 | html,
6 | body {
7 | height: 100%;
8 | width: 100%;
9 | }
10 |
11 | body {
12 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
13 | }
14 |
15 | body.fontLoaded {
16 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
17 | }
18 |
19 | #app {
20 | background-color: #fafafa;
21 | min-height: 100%;
22 | min-width: 100%;
23 | }
24 |
25 | p,
26 | label {
27 | font-family: Georgia, Times, 'Times New Roman', serif;
28 | line-height: 1.5em;
29 | }
30 | `;
31 |
--------------------------------------------------------------------------------
/app/i18n.js:
--------------------------------------------------------------------------------
1 | /**
2 | * i18n.js
3 | *
4 | * This will setup the i18n language files and locale data for your app.
5 | *
6 | */
7 | import { addLocaleData } from 'react-intl';
8 | import enLocaleData from 'react-intl/locale-data/en';
9 | import deLocaleData from 'react-intl/locale-data/de';
10 |
11 | import { DEFAULT_LOCALE } from '../app/containers/App/constants';
12 |
13 | import enTranslationMessages from './translations/en.json';
14 | import deTranslationMessages from './translations/de.json';
15 |
16 | addLocaleData(enLocaleData);
17 | addLocaleData(deLocaleData);
18 |
19 | export const appLocales = [
20 | 'en',
21 | 'de',
22 | ];
23 |
24 | export const formatTranslationMessages = (locale, messages) => {
25 | const defaultFormattedMessages = locale !== DEFAULT_LOCALE
26 | ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages)
27 | : {};
28 | return Object.keys(messages).reduce((formattedMessages, key) => {
29 | const formattedMessage = !messages[key] && locale !== DEFAULT_LOCALE
30 | ? defaultFormattedMessages[key]
31 | : messages[key];
32 | return Object.assign(formattedMessages, { [key]: formattedMessage });
33 | }, {});
34 | };
35 |
36 | export const translationMessages = {
37 | en: formatTranslationMessages('en', enTranslationMessages),
38 | de: formatTranslationMessages('de', deTranslationMessages),
39 | };
40 |
--------------------------------------------------------------------------------
/app/images/AppTray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/AppTray.png
--------------------------------------------------------------------------------
/app/images/SharedIndexDataFlow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/SharedIndexDataFlow.png
--------------------------------------------------------------------------------
/app/images/SharedIndexFlow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/SharedIndexFlow.png
--------------------------------------------------------------------------------
/app/images/SharedIndexFuture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/SharedIndexFuture.png
--------------------------------------------------------------------------------
/app/images/StealthyV1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/StealthyV1.png
--------------------------------------------------------------------------------
/app/images/blue128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/blue128.png
--------------------------------------------------------------------------------
/app/images/blue256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/blue256.png
--------------------------------------------------------------------------------
/app/images/blue512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/blue512.png
--------------------------------------------------------------------------------
/app/images/custom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/custom.png
--------------------------------------------------------------------------------
/app/images/dappstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/dappstore.png
--------------------------------------------------------------------------------
/app/images/defaultProfile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/defaultProfile.png
--------------------------------------------------------------------------------
/app/images/elliot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/elliot.jpg
--------------------------------------------------------------------------------
/app/images/elliot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/elliot.png
--------------------------------------------------------------------------------
/app/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/favicon.ico
--------------------------------------------------------------------------------
/app/images/homePage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/homePage.png
--------------------------------------------------------------------------------
/app/images/image16x9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/image16x9.png
--------------------------------------------------------------------------------
/app/images/laptopChat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/laptopChat.png
--------------------------------------------------------------------------------
/app/images/msgBack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/msgBack.png
--------------------------------------------------------------------------------
/app/images/plugin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/plugin.jpg
--------------------------------------------------------------------------------
/app/images/rStealthyFlow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/rStealthyFlow.jpg
--------------------------------------------------------------------------------
/app/images/snowden.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/snowden.jpg
--------------------------------------------------------------------------------
/app/images/stealthyFlow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/stealthyFlow.png
--------------------------------------------------------------------------------
/app/images/stealthyMobileFlow2.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/stealthyMobileFlow2.mp4
--------------------------------------------------------------------------------
/app/images/stealthy_plane.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/app/images/stock/blank.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/stock/blank.jpg
--------------------------------------------------------------------------------
/app/images/stock/bug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/stock/bug.png
--------------------------------------------------------------------------------
/app/images/videoPoster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/videoPoster.png
--------------------------------------------------------------------------------
/app/images/whit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stealthyinc/webapp/e0d4a8e411d488fac8b440dcebb0c6922057b75c/app/images/whit.png
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Stealthy",
3 | "theme_color": "#b1624d",
4 | "background_color": "#fafafa",
5 | "description": "Stealthy | A Blockstack Chat Client",
6 | "display": "standalone",
7 | "start_url": "https://www.stealthy.im/",
8 | "icons": [
9 | {
10 | "src": "https://www.stealthy.im/blue512.png",
11 | "sizes": "72x72",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "https://www.stealthy.im/blue512.png",
16 | "sizes": "96x96",
17 | "type": "image/png"
18 | },
19 | {
20 | "src": "https://www.stealthy.im/blue512.png",
21 | "sizes": "128x128",
22 | "type": "image/png"
23 | },
24 | {
25 | "src": "https://www.stealthy.im/blue512.png",
26 | "sizes": "144x144",
27 | "type": "image/png"
28 | },
29 | {
30 | "src": "https://www.stealthy.im/blue512.png",
31 | "sizes": "152x152",
32 | "type": "image/png"
33 | },
34 | {
35 | "src": "https://www.stealthy.im/blue512.png",
36 | "sizes": "192x192",
37 | "type": "image/png"
38 | },
39 | {
40 | "src": "https://www.stealthy.im/blue512.png",
41 | "sizes": "384x384",
42 | "type": "image/png"
43 | },
44 | {
45 | "src": "https://www.stealthy.im/blue512.png",
46 | "sizes": "512x512",
47 | "type": "image/png"
48 | }
49 | ],
50 | "splash_pages": null
51 | }
52 |
--------------------------------------------------------------------------------
/app/messaging/chatMessage.js:
--------------------------------------------------------------------------------
1 | // TODO: Going forward, look into making this XMPP using their spec. etc.
2 | //
3 |
4 | // Non-XMPP:
5 | const MESSAGE_TYPE = {
6 | TEXT: 0,
7 | VIDEO_SDP: 1,
8 | DATA: 2,
9 | RECEIPT: 3,
10 | SCREEN_SHARE_SDP: 4,
11 | };
12 |
13 | // RECEIVED is not used in the UI, but is used by the core to perform message
14 | // deletion.
15 | // SENDING is the default state in the GUI.
16 | //
17 | const MESSAGE_STATE = {
18 | UNDEFINED: '',
19 | SENDING: 'sending',
20 | SENT_REALTIME: 'sentRealtime',
21 | SENT_OFFLINE: 'sentOffline',
22 | RECEIVED: 'received',
23 | SEEN: 'seen',
24 | };
25 |
26 | class ChatMessage {
27 | constructor() {
28 | this.from = undefined;
29 | this.to = undefined;
30 | this.id = undefined;
31 | this.content = undefined;
32 |
33 | this.time = undefined;
34 | // this.timeRcvd = timeRcvd;
35 | this.sent = undefined;
36 | this.seen = undefined;
37 | this.type = undefined;
38 |
39 | this.msgState = MESSAGE_STATE.UNDEFINED;
40 | }
41 |
42 | init(
43 | from = undefined,
44 | to = undefined,
45 | id = undefined,
46 | content = undefined,
47 | //
48 | // Non-XMPP:
49 | time = undefined,
50 | // timeRcvd = undefined,
51 | type = MESSAGE_TYPE.TEXT,
52 | sent = false,
53 | seen = false,
54 | msgState = MESSAGE_STATE.UNDEFINED,
55 | // gaia=undefined, // i.e. is stored? (archived is taken below)
56 | //
57 | // Future (XMPP):
58 | // format=undefined,
59 | // state=undefined,
60 | // delay=undefined,
61 | // archived=undefined,
62 | ) {
63 | this.from = from;
64 | this.to = to;
65 | this.id = id;
66 | this.content = content;
67 |
68 | this.time = time;
69 | // this.timeRcvd = timeRcvd;
70 | this.sent = sent;
71 | this.seen = seen;
72 | this.type = type;
73 |
74 | this.msgState = msgState;
75 | }
76 |
77 | clone(aChatMessage) {
78 | this.from = aChatMessage.from;
79 | this.to = aChatMessage.to;
80 | this.id = aChatMessage.id;
81 | this.content = aChatMessage.content;
82 |
83 | this.time = aChatMessage.time;
84 | this.sent = aChatMessage.sent;
85 | this.seen = aChatMessage.seen;
86 | this.type = aChatMessage.type;
87 |
88 | this.msgState = aChatMessage.msgState;
89 | }
90 | }
91 |
92 | module.exports = { MESSAGE_TYPE, MESSAGE_STATE, ChatMessage };
93 |
--------------------------------------------------------------------------------
/app/network/avPeerMgr.js:
--------------------------------------------------------------------------------
1 | class AVPeerMgr {
2 | constructor(userId,
3 | targetUserId,
4 | initiator = false) {
5 | this.userId = userId;
6 | this.targetUserId = targetUserId;
7 | this.self = (this.userId === this.targetUserId);
8 | this.initiator = initiator;
9 | this.sdpInvite = undefined;
10 | this.sdpResponse = undefined;
11 | this.peerObjInitiator = undefined;
12 | this.peerObjResponder = undefined;
13 | this.stream = undefined;
14 | }
15 |
16 | getUserId() {
17 | return this.userId;
18 | }
19 |
20 | getTargetId() {
21 | return this.targetUserId;
22 | }
23 |
24 | setInitiator(isInitiator) {
25 | this.initiator = isInitiator;
26 | }
27 |
28 | isInitiator() {
29 | return this.initiator;
30 | }
31 |
32 | isSelf() {
33 | return this.self;
34 | }
35 |
36 | setPeerObjInitiator(aPeerObj) {
37 | this.peerObjInitiator = aPeerObj;
38 | }
39 |
40 | getPeerObjInitiator() {
41 | return this.peerObjInitiator;
42 | }
43 |
44 | setPeerObjResponder(aPeerObj) {
45 | this.peerObjResponder = aPeerObj;
46 | }
47 |
48 | getPeerObjResponder() {
49 | return this.peerObjResponder;
50 | }
51 |
52 | setPeerObj(aPeerObj) {
53 | if (this.initiator) {
54 | this.peerObjInitiator = aPeerObj;
55 | } else {
56 | this.peerObjResponder = aPeerObj;
57 | }
58 | }
59 |
60 | setSdpInvite(anSdpObj) {
61 | this.sdpInvite = anSdpObj;
62 | }
63 |
64 | getSdpInvite() {
65 | return this.sdpInvite;
66 | }
67 |
68 | setSdpResponse(anSdpObj) {
69 | this.sdpResponse = anSdpObj;
70 | }
71 |
72 | getSdpResponse() {
73 | return this.sdpResponse;
74 | }
75 |
76 | setStream(aStream) {
77 | this.stream = aStream;
78 | }
79 |
80 | close() {
81 | if (this.stream) {
82 | const vidTracks = this.stream.getVideoTracks();
83 | if (vidTracks && (vidTracks.length > 0)) {
84 | for (const vidTrack of vidTracks) {
85 | vidTrack.stop();
86 | }
87 | }
88 |
89 | const audTracks = this.stream.getAudioTracks();
90 | if (audTracks && (audTracks.length > 0)) {
91 | for (const audTrack of audTracks) {
92 | audTrack.stop();
93 | }
94 | }
95 | }
96 |
97 | if (this.peerObjInitiator) {
98 | this.peerObjInitiator.destroy();
99 | }
100 |
101 | if (this.peerObjResponder) {
102 | this.peerObjResponder.destroy();
103 | }
104 | }
105 | }
106 |
107 | module.exports = { AVPeerMgr };
108 |
--------------------------------------------------------------------------------
/app/network/sdpManager.js:
--------------------------------------------------------------------------------
1 | const INVITE_FN = 'invite.json';
2 | const RESPONSE_FN = 'response.json';
3 |
4 | module.exports = class SdpManager {
5 | constructor(user, ioClassInst) {
6 | this.user = user;
7 |
8 | this.ioInst = ioClassInst;
9 | }
10 |
11 | writeSdpInvite(targetUser, sdpObj) {
12 | const inviteFileName = `${targetUser}/${INVITE_FN}`;
13 | return this.ioInst.writeLocalFile(this.user, inviteFileName, sdpObj);
14 | }
15 |
16 | readSdpInvite(targetUser) {
17 | const inviteFileName = `${this.user}/${INVITE_FN}`;
18 | return this.ioInst.readRemoteFile(targetUser, inviteFileName);
19 | }
20 |
21 | writeSdpResponse(targetUser, sdpObj) {
22 | const responseFileName = `${targetUser}/${RESPONSE_FN}`;
23 | return this.ioInst.writeLocalFile(this.user, responseFileName, sdpObj);
24 | }
25 |
26 | readSdpResponse(targetUser) {
27 | const responseFileName = `${this.user}/${RESPONSE_FN}`;
28 | return this.ioInst.readRemoteFile(targetUser, responseFileName);
29 | }
30 |
31 | deleteSdpInvite(targetUser) {
32 | const inviteFileName = `${targetUser}/${INVITE_FN}`;
33 | return this.ioInst.deleteLocalFile(this.user, inviteFileName);
34 | }
35 |
36 | deleteSdpResponse(targetUser) {
37 | const responseFileName = `${targetUser}/${RESPONSE_FN}`;
38 | return this.ioInst.deleteLocalFile(this.user, responseFileName);
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/app/network/utils.js:
--------------------------------------------------------------------------------
1 | // Safrari (Safari) currently doesn't work--ice failures. Here are things I've
2 | // tried so far (to no avail):
3 | // - https://github.com/feross/simple-peer/issues/206
4 | //
5 |
6 | function getDefaultSimplePeerOpts(initiator = true) {
7 | return {
8 | initiator,
9 | trickle: false,
10 | reconnectTimer: 100,
11 | iceTransportPolicy: 'relay',
12 | config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },
13 | };
14 | }
15 |
16 | function getSimplePeerOpts(initiator = true, anObject = undefined) {
17 | const simplePeerOpts = module.exports.getDefaultSimplePeerOpts(initiator);
18 |
19 | if (anObject) {
20 | for (const property in anObject) {
21 | simplePeerOpts[property] = anObject[property];
22 | }
23 | }
24 |
25 | return simplePeerOpts;
26 | }
27 |
28 | module.exports = { getDefaultSimplePeerOpts, getSimplePeerOpts };
29 |
--------------------------------------------------------------------------------
/app/redirect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Hello, Stealthy!
5 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/reducers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Combine all reducers in this file and export the combined reducers.
3 | */
4 |
5 | import { combineReducers } from 'redux';
6 | import { routerReducer } from 'react-router-redux';
7 |
8 | import globalReducer from 'containers/App/reducer';
9 | import toolBarReducer from 'containers/ToolBar/reducer';
10 | import messagePageReducer from 'containers/MessagePage/reducer';
11 | import contactListReducer from 'containers/ContactList/reducer';
12 | import messageListReducer from 'containers/MessageList/reducer';
13 | import blockPageReducer from 'containers/BlockPage/reducer';
14 | import languageProviderReducer from 'containers/LanguageProvider/reducer';
15 |
16 | /**
17 | * Creates the main reducer with the dynamically injected ones
18 | */
19 | export default function createReducer(injectedReducers) {
20 | return combineReducers({
21 | routing: routerReducer,
22 | global: globalReducer,
23 | language: languageProviderReducer,
24 | toolBar: toolBarReducer,
25 | contactList: contactListReducer,
26 | messagePage: messagePageReducer,
27 | messageList: messageListReducer,
28 | blockPage: blockPageReducer,
29 | ...injectedReducers,
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/app/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/app/tests/i18n.test.js:
--------------------------------------------------------------------------------
1 | import { DEFAULT_LOCALE } from '../containers/App/constants';
2 | import { formatTranslationMessages } from '../i18n';
3 |
4 | jest.mock('../translations/en.json', () => (
5 | {
6 | message1: 'default message',
7 | message2: 'default message 2',
8 | }
9 | ));
10 |
11 | const esTranslationMessages = {
12 | message1: 'mensaje predeterminado',
13 | message2: '',
14 | };
15 |
16 | describe('formatTranslationMessages', () => {
17 | it('should build only defaults when DEFAULT_LOCALE', () => {
18 | const result = formatTranslationMessages(DEFAULT_LOCALE, { a: 'a' });
19 |
20 | expect(result).toEqual({ a: 'a' });
21 | });
22 |
23 |
24 | it('should combine default locale and current locale when not DEFAULT_LOCALE', () => {
25 | const result = formatTranslationMessages('', esTranslationMessages);
26 |
27 | expect(result).toEqual({
28 | message1: 'mensaje predeterminado',
29 | message2: 'default message 2',
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/app/tests/store.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test store addons
3 | */
4 |
5 | import { browserHistory } from 'react-router-dom';
6 | import configureStore from '../configureStore';
7 |
8 | describe('configureStore', () => {
9 | let store;
10 |
11 | beforeAll(() => {
12 | store = configureStore({}, browserHistory);
13 | });
14 |
15 | describe('injectedReducers', () => {
16 | it('should contain an object for reducers', () => {
17 | expect(typeof store.injectedReducers).toBe('object');
18 | });
19 | });
20 |
21 | describe('injectedSagas', () => {
22 | it('should contain an object for sagas', () => {
23 | expect(typeof store.injectedSagas).toBe('object');
24 | });
25 | });
26 |
27 | describe('runSaga', () => {
28 | it('should contain a hook for `sagaMiddleware.run`', () => {
29 | expect(typeof store.runSaga).toBe('function');
30 | });
31 | });
32 | });
33 |
34 | describe('configureStore params', () => {
35 | it('should call window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__', () => {
36 | /* eslint-disable no-underscore-dangle */
37 | const compose = jest.fn();
38 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = () => compose;
39 | configureStore(undefined, browserHistory);
40 | expect(compose).toHaveBeenCalled();
41 | /* eslint-enable */
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/app/utils/checkStore.js:
--------------------------------------------------------------------------------
1 | import conformsTo from 'lodash/conformsTo';
2 | import isFunction from 'lodash/isFunction';
3 | import isObject from 'lodash/isObject';
4 | import invariant from 'invariant';
5 |
6 | /**
7 | * Validate the shape of redux store
8 | */
9 | export default function checkStore(store) {
10 | const shape = {
11 | dispatch: isFunction,
12 | subscribe: isFunction,
13 | getState: isFunction,
14 | replaceReducer: isFunction,
15 | runSaga: isFunction,
16 | injectedReducers: isObject,
17 | injectedSagas: isObject,
18 | };
19 | invariant(
20 | conformsTo(store, shape),
21 | '(app/utils...) injectors: Expected a valid redux store'
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/utils/common.js:
--------------------------------------------------------------------------------
1 | // import {Platform} from 'react-native';
2 | const platform = require('platform');
3 |
4 | export const NO_SESSION = 'none'
5 |
6 | export const getSessionRef = function(aPublicKey) {
7 | // ud --> user data:
8 | return `${getRootRef(aPublicKey)}/session`
9 | };
10 |
11 | export const getRootRef = function(aPublicKey) {
12 | // ud --> user data:
13 | return (process.env.NODE_ENV === 'production') ?
14 | `/global/ud/${aPublicKey}` :
15 | `/global/development/ud/${aPublicKey}`
16 | };
17 |
18 | var __sessionId = undefined;
19 | //
20 | export const getSessionId = function() {
21 | if (!__sessionId) {
22 | const { name } = platform;
23 | __sessionId = `${name}-${Date.now()}`
24 | }
25 |
26 | console.log(`INFO(common.js::getSessionId): returning ${__sessionId}`)
27 | return __sessionId
28 | };
29 |
--------------------------------------------------------------------------------
/app/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount';
2 | export const DAEMON = '@@saga-injector/daemon';
3 | export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount';
4 |
--------------------------------------------------------------------------------
/app/utils/cryptoUtils.js:
--------------------------------------------------------------------------------
1 | // TODO: crypto is now a built in. Remove this require and the assoc
2 | // npm package.
3 | const crypto = require('crypto');
4 | const algorithm = 'aes-256-ctr';
5 |
6 | // Nodejs encryption with CTR
7 | // (from: https://github.com/chris-rock/node-crypto-examples/blob/master/crypto-ctr.js)
8 | exports.encryptStr = function (text, password) {
9 | const cipher = crypto.createCipher(algorithm, password);
10 | let crypted = cipher.update(text, 'utf8', 'hex');
11 | crypted += cipher.final('hex');
12 | return crypted;
13 | };
14 |
15 | exports.decryptStr = function (text, password) {
16 | const decipher = crypto.createDecipher(algorithm, password);
17 | let dec = decipher.update(text, 'hex', 'utf8');
18 | dec += decipher.final('utf8');
19 | return dec;
20 | };
21 |
--------------------------------------------------------------------------------
/app/utils/getQueryString.js:
--------------------------------------------------------------------------------
1 | export default function getQueryString(field, url) {
2 | const href = url || window.location.href;
3 | const reg = new RegExp(`[?&]${field}=([^]*)`, 'i');
4 | const string = reg.exec(href);
5 | return string ? string[1] : null;
6 | }
7 |
--------------------------------------------------------------------------------
/app/utils/injectReducer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import hoistNonReactStatics from 'hoist-non-react-statics';
4 |
5 | import getInjectors from './reducerInjectors';
6 |
7 | /**
8 | * Dynamically injects a reducer
9 | *
10 | * @param {string} key A key of the reducer
11 | * @param {function} reducer A reducer that will be injected
12 | *
13 | */
14 | export default ({ key, reducer }) => (WrappedComponent) => {
15 | class ReducerInjector extends React.Component {
16 | static WrappedComponent = WrappedComponent;
17 | static contextTypes = {
18 | store: PropTypes.object.isRequired,
19 | };
20 | static displayName = `withReducer(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;
21 |
22 | componentWillMount() {
23 | const { injectReducer } = this.injectors;
24 |
25 | injectReducer(key, reducer);
26 | }
27 |
28 | injectors = getInjectors(this.context.store);
29 |
30 | render() {
31 | return
;
32 | }
33 | }
34 |
35 | return hoistNonReactStatics(ReducerInjector, WrappedComponent);
36 | };
37 |
--------------------------------------------------------------------------------
/app/utils/injectSaga.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import hoistNonReactStatics from 'hoist-non-react-statics';
4 |
5 | import getInjectors from './sagaInjectors';
6 |
7 | /**
8 | * Dynamically injects a saga, passes component's props as saga arguments
9 | *
10 | * @param {string} key A key of the saga
11 | * @param {function} saga A root saga that will be injected
12 | * @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and
13 | * cancelled with `task.cancel()` on component un-mount for improved performance. Another two options:
14 | * - constants.DAEMON—starts the saga on component mount and never cancels it or starts again,
15 | * - constants.ONCE_TILL_UNMOUNT—behaves like 'RESTART_ON_REMOUNT' but never runs it again.
16 | *
17 | */
18 | export default ({ key, saga, mode }) => (WrappedComponent) => {
19 | class InjectSaga extends React.Component {
20 | static WrappedComponent = WrappedComponent;
21 | static contextTypes = {
22 | store: PropTypes.object.isRequired,
23 | };
24 | static displayName = `withSaga(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;
25 |
26 | componentWillMount() {
27 | const { injectSaga } = this.injectors;
28 |
29 | injectSaga(key, { saga, mode }, this.props);
30 | }
31 |
32 | componentWillUnmount() {
33 | const { ejectSaga } = this.injectors;
34 |
35 | ejectSaga(key);
36 | }
37 |
38 | injectors = getInjectors(this.context.store);
39 |
40 | render() {
41 | return
;
42 | }
43 | }
44 |
45 | return hoistNonReactStatics(InjectSaga, WrappedComponent);
46 | };
47 |
--------------------------------------------------------------------------------
/app/utils/notification.js:
--------------------------------------------------------------------------------
1 | import icon from '../images/icon-128x128.png';
2 |
3 | export default function notification(data) {
4 | // https://developer.mozilla.org/en-US/docs/Web/API/notification
5 | // Let's check if the browser supports notifications
6 | if (!('Notification' in window)) {
7 | alert('This browser does not support desktop notification');
8 | }
9 |
10 | // Let's check whether notification permissions have already been granted
11 | else if (Notification.permission === 'granted') {
12 | // If it's okay let's create a notification
13 | const { title, body, tag } = data;
14 | const notification = new Notification(title, { body, tag, icon });
15 | }
16 |
17 | // Otherwise, we need to ask the user for permission
18 | else if (Notification.permission !== 'denied') {
19 | Notification.requestPermission((permission) => {
20 | // If the user accepts, let's create a notification
21 | if (permission === 'granted') {
22 | const { title, body, tag } = data;
23 | const notification = new Notification(title, { body, tag, icon });
24 | }
25 | });
26 | }
27 |
28 | // At last, if the user has denied notifications, and you
29 | // want to be respectful there is no need to bother them any more.
30 | }
31 |
--------------------------------------------------------------------------------
/app/utils/reducerInjectors.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant';
2 | import isEmpty from 'lodash/isEmpty';
3 | import isFunction from 'lodash/isFunction';
4 | import isString from 'lodash/isString';
5 |
6 | import checkStore from './checkStore';
7 | import createReducer from '../reducers';
8 |
9 | export function injectReducerFactory(store, isValid) {
10 | return function injectReducer(key, reducer) {
11 | if (!isValid) checkStore(store);
12 |
13 | invariant(
14 | isString(key) && !isEmpty(key) && isFunction(reducer),
15 | '(app/utils...) injectReducer: Expected `reducer` to be a reducer function'
16 | );
17 |
18 | // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
19 | if (Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer) return;
20 |
21 | store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
22 | store.replaceReducer(createReducer(store.injectedReducers));
23 | };
24 | }
25 |
26 | export default function getInjectors(store) {
27 | checkStore(store);
28 |
29 | return {
30 | injectReducer: injectReducerFactory(store, true),
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/app/utils/request.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 |
3 | /**
4 | * Parses the JSON returned by a network request
5 | *
6 | * @param {object} response A response from a network request
7 | *
8 | * @return {object} The parsed JSON from the request
9 | */
10 | function parseJSON(response) {
11 | if (response.status === 204 || response.status === 205) {
12 | return null;
13 | }
14 | return response.json();
15 | }
16 |
17 | /**
18 | * Checks if a network request came back fine, and throws an error if not
19 | *
20 | * @param {object} response A response from a network request
21 | *
22 | * @return {object|undefined} Returns either the response, or throws an error
23 | */
24 | function checkStatus(response) {
25 | if (response.status >= 200 && response.status < 300) {
26 | return response;
27 | }
28 |
29 | const error = new Error(response.statusText);
30 | error.response = response;
31 | throw error;
32 | }
33 |
34 | /**
35 | * Requests a URL, returning a promise
36 | *
37 | * @param {string} url The URL we want to request
38 | * @param {object} [options] The options we want to pass to "fetch"
39 | *
40 | * @return {object} The response data
41 | */
42 | export default function request(url, options) {
43 | return fetch(url, options)
44 | .then(checkStatus)
45 | .then(parseJSON);
46 | }
47 |
--------------------------------------------------------------------------------
/app/utils/tests/checkStore.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test injectors
3 | */
4 |
5 | import checkStore from '../checkStore';
6 |
7 | describe('checkStore', () => {
8 | let store;
9 |
10 | beforeEach(() => {
11 | store = {
12 | dispatch: () => {},
13 | subscribe: () => {},
14 | getState: () => {},
15 | replaceReducer: () => {},
16 | runSaga: () => {},
17 | injectedReducers: {},
18 | injectedSagas: {},
19 | };
20 | });
21 |
22 | it('should not throw if passed valid store shape', () => {
23 | expect(() => checkStore(store)).not.toThrow();
24 | });
25 |
26 | it('should throw if passed invalid store shape', () => {
27 | expect(() => checkStore({})).toThrow();
28 | expect(() => checkStore({ ...store, injectedSagas: null })).toThrow();
29 | expect(() => checkStore({ ...store, injectedReducers: null })).toThrow();
30 | expect(() => checkStore({ ...store, runSaga: null })).toThrow();
31 | expect(() => checkStore({ ...store, replaceReducer: null })).toThrow();
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/app/utils/tests/injectReducer.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test injectors
3 | */
4 |
5 | import { memoryHistory } from 'react-router-dom';
6 | import { shallow } from 'enzyme';
7 | import React from 'react';
8 | import identity from 'lodash/identity';
9 |
10 | import configureStore from '../../configureStore';
11 | import injectReducer from '../injectReducer';
12 | import * as reducerInjectors from '../reducerInjectors';
13 |
14 | // Fixtures
15 | const Component = () => null;
16 |
17 | const reducer = identity;
18 |
19 | describe('injectReducer decorator', () => {
20 | let store;
21 | let injectors;
22 | let ComponentWithReducer;
23 |
24 | beforeAll(() => {
25 | reducerInjectors.default = jest.fn().mockImplementation(() => injectors);
26 | });
27 |
28 | beforeEach(() => {
29 | store = configureStore({}, memoryHistory);
30 | injectors = {
31 | injectReducer: jest.fn(),
32 | };
33 | ComponentWithReducer = injectReducer({ key: 'test', reducer })(Component);
34 | reducerInjectors.default.mockClear();
35 | });
36 |
37 | it('should inject a given reducer', () => {
38 | shallow(
, { context: { store } });
39 |
40 | expect(injectors.injectReducer).toHaveBeenCalledTimes(1);
41 | expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer);
42 | });
43 |
44 | it('should set a correct display name', () => {
45 | expect(ComponentWithReducer.displayName).toBe('withReducer(Component)');
46 | expect(injectReducer({ key: 'test', reducer })(() => null).displayName).toBe('withReducer(Component)');
47 | });
48 |
49 | it('should propagate props', () => {
50 | const props = { testProp: 'test' };
51 | const renderedComponent = shallow(
, { context: { store } });
52 |
53 | expect(renderedComponent.prop('testProp')).toBe('test');
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/app/utils/tests/injectSaga.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test injectors
3 | */
4 |
5 | import { memoryHistory } from 'react-router-dom';
6 | import { put } from 'redux-saga/effects';
7 | import { shallow } from 'enzyme';
8 | import React from 'react';
9 |
10 | import configureStore from '../../configureStore';
11 | import injectSaga from '../injectSaga';
12 | import * as sagaInjectors from '../sagaInjectors';
13 |
14 | // Fixtures
15 | const Component = () => null;
16 |
17 | function* testSaga() {
18 | yield put({ type: 'TEST', payload: 'yup' });
19 | }
20 |
21 | describe('injectSaga decorator', () => {
22 | let store;
23 | let injectors;
24 | let ComponentWithSaga;
25 |
26 | beforeAll(() => {
27 | sagaInjectors.default = jest.fn().mockImplementation(() => injectors);
28 | });
29 |
30 | beforeEach(() => {
31 | store = configureStore({}, memoryHistory);
32 | injectors = {
33 | injectSaga: jest.fn(),
34 | ejectSaga: jest.fn(),
35 | };
36 | ComponentWithSaga = injectSaga({ key: 'test', saga: testSaga, mode: 'testMode' })(Component);
37 | sagaInjectors.default.mockClear();
38 | });
39 |
40 | it('should inject given saga, mode, and props', () => {
41 | const props = { test: 'test' };
42 | shallow(
, { context: { store } });
43 |
44 | expect(injectors.injectSaga).toHaveBeenCalledTimes(1);
45 | expect(injectors.injectSaga).toHaveBeenCalledWith('test', { saga: testSaga, mode: 'testMode' }, props);
46 | });
47 |
48 | it('should eject on unmount with a correct saga key', () => {
49 | const props = { test: 'test' };
50 | const renderedComponent = shallow(
, { context: { store } });
51 | renderedComponent.unmount();
52 |
53 | expect(injectors.ejectSaga).toHaveBeenCalledTimes(1);
54 | expect(injectors.ejectSaga).toHaveBeenCalledWith('test');
55 | });
56 |
57 | it('should set a correct display name', () => {
58 | expect(ComponentWithSaga.displayName).toBe('withSaga(Component)');
59 | expect(injectSaga({ key: 'test', saga: testSaga })(() => null).displayName).toBe('withSaga(Component)');
60 | });
61 |
62 | it('should propagate props', () => {
63 | const props = { testProp: 'test' };
64 | const renderedComponent = shallow(
, { context: { store } });
65 |
66 | expect(renderedComponent.prop('testProp')).toBe('test');
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/app/utils/tests/request.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test the request function
3 | */
4 |
5 | import request from '../request';
6 |
7 | describe('request', () => {
8 | // Before each test, stub the fetch function
9 | beforeEach(() => {
10 | window.fetch = jest.fn();
11 | });
12 |
13 | describe('stubbing successful response', () => {
14 | // Before each test, pretend we got a successful response
15 | beforeEach(() => {
16 | const res = new Response('{"hello":"world"}', {
17 | status: 200,
18 | headers: {
19 | 'Content-type': 'application/json',
20 | },
21 | });
22 |
23 | window.fetch.mockReturnValue(Promise.resolve(res));
24 | });
25 |
26 | it('should format the response correctly', (done) => {
27 | request('/thisurliscorrect')
28 | .catch(done)
29 | .then((json) => {
30 | expect(json.hello).toBe('world');
31 | done();
32 | });
33 | });
34 | });
35 |
36 | describe('stubbing 204 response', () => {
37 | // Before each test, pretend we got a successful response
38 | beforeEach(() => {
39 | const res = new Response('', {
40 | status: 204,
41 | statusText: 'No Content',
42 | });
43 |
44 | window.fetch.mockReturnValue(Promise.resolve(res));
45 | });
46 |
47 | it('should return null on 204 response', (done) => {
48 | request('/thisurliscorrect')
49 | .catch(done)
50 | .then((json) => {
51 | expect(json).toBeNull();
52 | done();
53 | });
54 | });
55 | });
56 |
57 | describe('stubbing error response', () => {
58 | // Before each test, pretend we got an unsuccessful response
59 | beforeEach(() => {
60 | const res = new Response('', {
61 | status: 404,
62 | statusText: 'Not Found',
63 | headers: {
64 | 'Content-type': 'application/json',
65 | },
66 | });
67 |
68 | window.fetch.mockReturnValue(Promise.resolve(res));
69 | });
70 |
71 | it('should catch errors', (done) => {
72 | request('/thisdoesntexist')
73 | .catch((err) => {
74 | expect(err.response.status).toBe(404);
75 | expect(err.response.statusText).toBe('Not Found');
76 | done();
77 | });
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------