aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/todos
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/todos')
-rw-r--r--src/features/todos/actions.js22
-rw-r--r--src/features/todos/components/TodosWebview.js237
-rw-r--r--src/features/todos/constants.js4
-rw-r--r--src/features/todos/containers/TodosScreen.js32
-rw-r--r--src/features/todos/index.js34
-rw-r--r--src/features/todos/preload.js23
-rw-r--r--src/features/todos/store.js147
7 files changed, 499 insertions, 0 deletions
diff --git a/src/features/todos/actions.js b/src/features/todos/actions.js
new file mode 100644
index 000000000..dc63d5fcd
--- /dev/null
+++ b/src/features/todos/actions.js
@@ -0,0 +1,22 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const todoActions = createActionsFromDefinitions({
5 resize: {
6 width: PropTypes.number.isRequired,
7 },
8 toggleTodosPanel: {},
9 setTodosWebview: {
10 webview: PropTypes.instanceOf(Element).isRequired,
11 },
12 handleHostMessage: {
13 action: PropTypes.string.isRequired,
14 data: PropTypes.object,
15 },
16 handleClientMessage: {
17 action: PropTypes.string.isRequired,
18 data: PropTypes.object,
19 },
20}, PropTypes.checkPropTypes);
21
22export default todoActions;
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js
new file mode 100644
index 000000000..288c1906f
--- /dev/null
+++ b/src/features/todos/components/TodosWebview.js
@@ -0,0 +1,237 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import Webview from 'react-electron-web-view';
6import { Icon } from '@meetfranz/ui';
7
8import * as environment from '../../../environment';
9
10const OPEN_TODOS_BUTTON_SIZE = 45;
11const CLOSE_TODOS_BUTTON_SIZE = 35;
12
13const styles = theme => ({
14 root: {
15 background: theme.colorBackground,
16 position: 'relative',
17 borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor],
18 zIndex: 300,
19
20 transform: ({ isVisible, width }) => `translateX(${isVisible ? 0 : width}px)`,
21
22 '&:hover $closeTodosButton': {
23 opacity: 1,
24 },
25 },
26 webview: {
27 height: '100%',
28
29 '& webview': {
30 height: '100%',
31 },
32 },
33 resizeHandler: {
34 position: 'absolute',
35 left: 0,
36 marginLeft: -5,
37 width: 10,
38 zIndex: 400,
39 cursor: 'col-resize',
40 },
41 dragIndicator: {
42 position: 'absolute',
43 left: 0,
44 width: 5,
45 zIndex: 400,
46 background: theme.todos.dragIndicator.background,
47
48 },
49 openTodosButton: {
50 width: OPEN_TODOS_BUTTON_SIZE,
51 height: OPEN_TODOS_BUTTON_SIZE,
52 background: theme.todos.toggleButton.background,
53 position: 'absolute',
54 bottom: 80,
55 right: props => (props.width + (props.isVisible ? -OPEN_TODOS_BUTTON_SIZE / 2 : 0)),
56 borderRadius: OPEN_TODOS_BUTTON_SIZE / 2,
57 opacity: props => (props.isVisible ? 0 : 1),
58 transition: 'right 0.5s',
59 zIndex: 600,
60 display: 'flex',
61 alignItems: 'center',
62 justifyContent: 'center',
63 boxShadow: [0, 0, 10, theme.todos.toggleButton.shadowColor],
64
65 borderTopRightRadius: props => (props.isVisible ? null : 0),
66 borderBottomRightRadius: props => (props.isVisible ? null : 0),
67
68 '& svg': {
69 fill: theme.todos.toggleButton.textColor,
70 transition: 'all 0.5s',
71 },
72 },
73 closeTodosButton: {
74 width: CLOSE_TODOS_BUTTON_SIZE,
75 height: CLOSE_TODOS_BUTTON_SIZE,
76 background: theme.todos.toggleButton.background,
77 position: 'absolute',
78 bottom: 80,
79 right: ({ width }) => (width + -CLOSE_TODOS_BUTTON_SIZE / 2),
80 borderRadius: CLOSE_TODOS_BUTTON_SIZE / 2,
81 opacity: 0,
82 transition: 'opacity 0.5s',
83 zIndex: 600,
84 display: 'flex',
85 alignItems: 'center',
86 justifyContent: 'center',
87 boxShadow: [0, 0, 10, theme.todos.toggleButton.shadowColor],
88
89 '& svg': {
90 fill: theme.todos.toggleButton.textColor,
91 },
92 },
93});
94
95@injectSheet(styles) @observer
96class TodosWebview extends Component {
97 static propTypes = {
98 classes: PropTypes.object.isRequired,
99 isVisible: PropTypes.bool.isRequired,
100 togglePanel: PropTypes.func.isRequired,
101 handleClientMessage: PropTypes.func.isRequired,
102 setTodosWebview: PropTypes.func.isRequired,
103 resize: PropTypes.func.isRequired,
104 width: PropTypes.number.isRequired,
105 minWidth: PropTypes.number.isRequired,
106 };
107
108 state = {
109 isDragging: false,
110 width: 300,
111 };
112
113 componentWillMount() {
114 const { width } = this.props;
115
116 this.setState({
117 width,
118 });
119 }
120
121 componentDidMount() {
122 this.node.addEventListener('mousemove', this.resizePanel.bind(this));
123 this.node.addEventListener('mouseup', this.stopResize.bind(this));
124 this.node.addEventListener('mouseleave', this.stopResize.bind(this));
125 }
126
127 startResize = (event) => {
128 this.setState({
129 isDragging: true,
130 initialPos: event.clientX,
131 delta: 0,
132 });
133 };
134
135 resizePanel(e) {
136 const { minWidth } = this.props;
137
138 const {
139 isDragging,
140 initialPos,
141 } = this.state;
142
143 if (isDragging && Math.abs(e.clientX - window.innerWidth) > minWidth) {
144 const delta = e.clientX - initialPos;
145
146 this.setState({
147 delta,
148 });
149 }
150 }
151
152 stopResize() {
153 const {
154 resize,
155 minWidth,
156 } = this.props;
157
158 const {
159 isDragging,
160 delta,
161 width,
162 } = this.state;
163
164 if (isDragging) {
165 let newWidth = width + (delta < 0 ? Math.abs(delta) : -Math.abs(delta));
166
167 if (newWidth < minWidth) {
168 newWidth = minWidth;
169 }
170
171 this.setState({
172 isDragging: false,
173 delta: 0,
174 width: newWidth,
175 });
176
177 resize(newWidth);
178 }
179 }
180
181 startListeningToIpcMessages() {
182 const { handleClientMessage } = this.props;
183 if (!this.webview) return;
184 this.webview.addEventListener('ipc-message', e => handleClientMessage(e.args[0]));
185 }
186
187 render() {
188 const {
189 classes, isVisible, togglePanel,
190 } = this.props;
191 const { width, delta, isDragging } = this.state;
192
193 return (
194 <>
195 <div
196 className={classes.root}
197 style={{ width: isVisible ? width : 0 }}
198 onMouseUp={() => this.stopResize()}
199 ref={(node) => { this.node = node; }}
200 >
201 <button
202 onClick={() => togglePanel()}
203 className={isVisible ? classes.closeTodosButton : classes.openTodosButton}
204 type="button"
205 >
206 <Icon icon={isVisible ? 'mdiChevronRight' : 'mdiCheckAll'} size={2} />
207 </button>
208 <div
209 className={classes.resizeHandler}
210 style={Object.assign({ left: delta }, isDragging ? { width: 600, marginLeft: -200 } : {})} // This hack is required as resizing with webviews beneath behaves quite bad
211 onMouseDown={e => this.startResize(e)}
212 />
213 {isDragging && (
214 <div
215 className={classes.dragIndicator}
216 style={{ left: delta }} // This hack is required as resizing with webviews beneath behaves quite bad
217 />
218 )}
219 <Webview
220 className={classes.webview}
221 onDidAttach={() => {
222 const { setTodosWebview } = this.props;
223 setTodosWebview(this.webview);
224 this.startListeningToIpcMessages();
225 }}
226 partition="persist:todos"
227 preload="./features/todos/preload.js"
228 ref={(webview) => { this.webview = webview ? webview.view : null; }}
229 src={environment.TODOS_FRONTEND}
230 />
231 </div>
232 </>
233 );
234 }
235}
236
237export default TodosWebview;
diff --git a/src/features/todos/constants.js b/src/features/todos/constants.js
new file mode 100644
index 000000000..2e8a431cc
--- /dev/null
+++ b/src/features/todos/constants.js
@@ -0,0 +1,4 @@
1export const IPC = {
2 TODOS_HOST_CHANNEL: 'TODOS_HOST_CHANNEL',
3 TODOS_CLIENT_CHANNEL: 'TODOS_CLIENT_CHANNEL',
4};
diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js
new file mode 100644
index 000000000..d071d0677
--- /dev/null
+++ b/src/features/todos/containers/TodosScreen.js
@@ -0,0 +1,32 @@
1import React, { Component } from 'react';
2import { observer } from 'mobx-react';
3
4import TodosWebview from '../components/TodosWebview';
5import ErrorBoundary from '../../../components/util/ErrorBoundary';
6import { TODOS_MIN_WIDTH, todosStore } from '..';
7import { todoActions } from '../actions';
8
9@observer
10class TodosScreen extends Component {
11 render() {
12 if (!todosStore || !todosStore.isFeatureActive) {
13 return null;
14 }
15
16 return (
17 <ErrorBoundary>
18 <TodosWebview
19 isVisible={todosStore.isTodosPanelVisible}
20 togglePanel={todoActions.toggleTodosPanel}
21 handleClientMessage={todoActions.handleClientMessage}
22 setTodosWebview={webview => todoActions.setTodosWebview({ webview })}
23 width={todosStore.width}
24 minWidth={TODOS_MIN_WIDTH}
25 resize={width => todoActions.resize({ width })}
26 />
27 </ErrorBoundary>
28 );
29 }
30}
31
32export default TodosScreen;
diff --git a/src/features/todos/index.js b/src/features/todos/index.js
new file mode 100644
index 000000000..00b165cc5
--- /dev/null
+++ b/src/features/todos/index.js
@@ -0,0 +1,34 @@
1import { reaction } from 'mobx';
2import TodoStore from './store';
3
4const debug = require('debug')('Franz:feature:todos');
5
6export const GA_CATEGORY_TODOS = 'Todos';
7
8export const DEFAULT_TODOS_WIDTH = 300;
9export const TODOS_MIN_WIDTH = 200;
10export const DEFAULT_TODOS_VISIBLE = true;
11
12export const todosStore = new TodoStore();
13
14export default function initTodos(stores, actions) {
15 stores.todos = todosStore;
16 const { features } = stores;
17
18 // Toggle todos feature
19 reaction(
20 () => features.features.isTodosEnabled,
21 (isEnabled) => {
22 if (isEnabled) {
23 debug('Initializing `todos` feature');
24 todosStore.start(stores, actions);
25 } else if (todosStore.isFeatureActive) {
26 debug('Disabling `todos` feature');
27 todosStore.stop();
28 }
29 },
30 {
31 fireImmediately: true,
32 },
33 );
34}
diff --git a/src/features/todos/preload.js b/src/features/todos/preload.js
new file mode 100644
index 000000000..6e38a2ef3
--- /dev/null
+++ b/src/features/todos/preload.js
@@ -0,0 +1,23 @@
1import { ipcRenderer } from 'electron';
2import { IPC } from './constants';
3
4const debug = require('debug')('Franz:feature:todos:preload');
5
6debug('Preloading Todos Webview');
7
8let hostMessageListener = () => {};
9
10window.franz = {
11 onInitialize(ipcHostMessageListener) {
12 hostMessageListener = ipcHostMessageListener;
13 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, { action: 'todos:initialized' });
14 },
15 sendToHost(message) {
16 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, message);
17 },
18};
19
20ipcRenderer.on(IPC.TODOS_HOST_CHANNEL, (event, message) => {
21 debug('Received host message', event, message);
22 hostMessageListener(message);
23});
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
new file mode 100644
index 000000000..acf95df0d
--- /dev/null
+++ b/src/features/todos/store.js
@@ -0,0 +1,147 @@
1import { ThemeType } from '@meetfranz/theme';
2import {
3 computed,
4 action,
5 observable,
6} from 'mobx';
7import localStorage from 'mobx-localstorage';
8
9import { todoActions } from './actions';
10import { FeatureStore } from '../utils/FeatureStore';
11import { createReactions } from '../../stores/lib/Reaction';
12import { createActionBindings } from '../utils/ActionBinding';
13import { DEFAULT_TODOS_WIDTH, TODOS_MIN_WIDTH, DEFAULT_TODOS_VISIBLE } from '.';
14import { IPC } from './constants';
15
16const debug = require('debug')('Franz:feature:todos:store');
17
18export default class TodoStore extends FeatureStore {
19 @observable isFeatureEnabled = false;
20
21 @observable isFeatureActive = false;
22
23 webview = null;
24
25 @computed get width() {
26 const width = this.settings.width || DEFAULT_TODOS_WIDTH;
27
28 return width < TODOS_MIN_WIDTH ? TODOS_MIN_WIDTH : width;
29 }
30
31 @computed get isTodosPanelVisible() {
32 if (this.settings.isTodosPanelVisible === undefined) return DEFAULT_TODOS_VISIBLE;
33
34 return this.settings.isTodosPanelVisible;
35 }
36
37 @computed get settings() {
38 return localStorage.getItem('todos') || {};
39 }
40
41 // ========== PUBLIC API ========= //
42
43 @action start(stores, actions) {
44 debug('TodoStore::start');
45 this.stores = stores;
46 this.actions = actions;
47
48 // ACTIONS
49
50 this._registerActions(createActionBindings([
51 [todoActions.resize, this._resize],
52 [todoActions.toggleTodosPanel, this._toggleTodosPanel],
53 [todoActions.setTodosWebview, this._setTodosWebview],
54 [todoActions.handleHostMessage, this._handleHostMessage],
55 [todoActions.handleClientMessage, this._handleClientMessage],
56 ]));
57
58 // REACTIONS
59
60 this._allReactions = createReactions([
61 this._setFeatureEnabledReaction,
62 ]);
63
64 this._registerReactions(this._allReactions);
65
66 this.isFeatureActive = true;
67 }
68
69 @action stop() {
70 super.stop();
71 debug('TodoStore::stop');
72 this.reset();
73 this.isFeatureActive = false;
74 }
75
76 // ========== PRIVATE METHODS ========= //
77
78 _updateSettings = (changes) => {
79 localStorage.setItem('todos', {
80 ...this.settings,
81 ...changes,
82 });
83 };
84
85 // Actions
86
87 @action _resize = ({ width }) => {
88 this._updateSettings({
89 width,
90 });
91 };
92
93 @action _toggleTodosPanel = () => {
94 this._updateSettings({
95 isTodosPanelVisible: !this.isTodosPanelVisible,
96 });
97 };
98
99 @action _setTodosWebview = ({ webview }) => {
100 debug('_setTodosWebview', webview);
101 this.webview = webview;
102 };
103
104 @action _handleHostMessage = (message) => {
105 debug('_handleHostMessage', message);
106 if (message.action === 'todos:create') {
107 this.webview.send(IPC.TODOS_HOST_CHANNEL, message);
108 }
109 };
110
111 @action _handleClientMessage = (message) => {
112 debug('_handleClientMessage', message);
113 switch (message.action) {
114 case 'todos:initialized': this._onTodosClientInitialized(); break;
115 case 'todos:goToService': this._goToService(message.data); break;
116 default:
117 debug('Unknown client message reiceived', message);
118 }
119 };
120
121 // Todos client message handlers
122
123 _onTodosClientInitialized = () => {
124 this.webview.send(IPC.TODOS_HOST_CHANNEL, {
125 action: 'todos:configure',
126 data: {
127 authToken: this.stores.user.authToken,
128 theme: this.stores.ui.isDarkThemeActive ? ThemeType.dark : ThemeType.default,
129 },
130 });
131 };
132
133 _goToService = ({ url, serviceId }) => {
134 if (url) {
135 this.stores.services.one(serviceId).webview.loadURL(url);
136 }
137 this.actions.service.setActive({ serviceId });
138 };
139
140 // Reactions
141
142 _setFeatureEnabledReaction = () => {
143 const { isTodosEnabled } = this.stores.features.features;
144
145 this.isFeatureEnabled = isTodosEnabled;
146 };
147}