aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/services/tabs/TabItem.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/services/tabs/TabItem.tsx')
-rw-r--r--src/components/services/tabs/TabItem.tsx411
1 files changed, 411 insertions, 0 deletions
diff --git a/src/components/services/tabs/TabItem.tsx b/src/components/services/tabs/TabItem.tsx
new file mode 100644
index 000000000..fae788764
--- /dev/null
+++ b/src/components/services/tabs/TabItem.tsx
@@ -0,0 +1,411 @@
1import { app, dialog, Menu } from '@electron/remote';
2import { Component } from 'react';
3import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
4import { inject, observer } from 'mobx-react';
5import classnames from 'classnames';
6import { SortableElement } from 'react-sortable-hoc';
7import injectSheet, { WithStylesProps } from 'react-jss';
8import ms from 'ms';
9
10import { autorun, makeObservable, observable, reaction } from 'mobx';
11import { mdiExclamation, mdiVolumeSource } from '@mdi/js';
12import Service from '../../../models/Service';
13import { altKey, cmdOrCtrlShortcutKey, shiftKey } from '../../../environment';
14import globalMessages from '../../../i18n/globalMessages';
15import Icon from '../../ui/icon';
16import { Stores } from '../../../@types/stores.types';
17import MenuItemConstructorOptions = Electron.MenuItemConstructorOptions;
18
19const IS_SERVICE_DEBUGGING_ENABLED = (
20 localStorage.getItem('debug') || ''
21).includes('Ferdium:Service');
22
23const messages = defineMessages({
24 reload: {
25 id: 'tabs.item.reload',
26 defaultMessage: 'Reload',
27 },
28 disableNotifications: {
29 id: 'tabs.item.disableNotifications',
30 defaultMessage: 'Disable notifications',
31 },
32 enableNotifications: {
33 id: 'tabs.item.enableNotification',
34 defaultMessage: 'Enable notifications',
35 },
36 disableAudio: {
37 id: 'tabs.item.disableAudio',
38 defaultMessage: 'Disable audio',
39 },
40 enableAudio: {
41 id: 'tabs.item.enableAudio',
42 defaultMessage: 'Enable audio',
43 },
44 enableDarkMode: {
45 id: 'tabs.item.enableDarkMode',
46 defaultMessage: 'Enable Dark mode',
47 },
48 disableDarkMode: {
49 id: 'tabs.item.disableDarkMode',
50 defaultMessage: 'Disable Dark mode',
51 },
52 disableService: {
53 id: 'tabs.item.disableService',
54 defaultMessage: 'Disable service',
55 },
56 enableService: {
57 id: 'tabs.item.enableService',
58 defaultMessage: 'Enable service',
59 },
60 hibernateService: {
61 id: 'tabs.item.hibernateService',
62 defaultMessage: 'Hibernate service',
63 },
64 wakeUpService: {
65 id: 'tabs.item.wakeUpService',
66 defaultMessage: 'Wake up service',
67 },
68 deleteService: {
69 id: 'tabs.item.deleteService',
70 defaultMessage: 'Delete service',
71 },
72 confirmDeleteService: {
73 id: 'tabs.item.confirmDeleteService',
74 defaultMessage: 'Do you really want to delete the {serviceName} service?',
75 },
76});
77
78let pollIndicatorTransition = 'none';
79let polledTransition = 'none';
80let pollAnsweredTransition = 'none';
81
82if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
83 pollIndicatorTransition = 'background 0.5s';
84 polledTransition = 'background 0.1s';
85 pollAnsweredTransition = 'background 0.1s';
86}
87
88const styles = {
89 pollIndicator: {
90 position: 'absolute',
91 bottom: 2,
92 width: 10,
93 height: 10,
94 borderRadius: 5,
95 background: 'gray',
96 transition: pollIndicatorTransition,
97 },
98 pollIndicatorPoll: {
99 left: 2,
100 },
101 pollIndicatorAnswer: {
102 left: 14,
103 },
104 polled: {
105 background: 'yellow !important',
106 transition: polledTransition,
107 },
108 pollAnswered: {
109 background: 'green !important',
110 transition: pollAnsweredTransition,
111 },
112 stale: {
113 background: 'red !important',
114 },
115};
116
117interface IProps extends WrappedComponentProps, WithStylesProps<typeof styles> {
118 showMessageBadgeWhenMutedSetting: boolean;
119 showServiceNameSetting: boolean;
120 showMessageBadgesEvenWhenMuted: boolean;
121 service: Service;
122 shortcutIndex: number;
123 stores?: Stores;
124 reload: () => void;
125
126 clickHandler: () => void;
127 toggleNotifications: () => void;
128 toggleAudio: () => void;
129 toggleDarkMode: () => void;
130 openSettings: (args: { path: string }) => void;
131 deleteService: () => void;
132 disableService: () => void;
133 enableService: () => void;
134 hibernateService: () => void;
135 wakeUpService: () => void;
136}
137
138interface IState {
139 showShortcutIndex: boolean;
140}
141
142@inject('stores')
143@observer
144class TabItem extends Component<IProps, IState> {
145 @observable isPolled = false;
146
147 @observable isPollAnswered = false;
148
149 constructor(props) {
150 super(props);
151
152 makeObservable(this);
153
154 this.state = {
155 showShortcutIndex: false,
156 };
157
158 reaction(
159 () => this.props.stores!.settings.app.enableLongPressServiceHint,
160 () => {
161 this.checkForLongPress(
162 this.props.stores!.settings.app.enableLongPressServiceHint,
163 );
164 },
165 );
166 }
167
168 handleShortcutIndex = (event, showShortcutIndex = true) => {
169 if (event.key === 'Shift') {
170 this.setState({ showShortcutIndex });
171 }
172 };
173
174 checkForLongPress = enableLongPressServiceHint => {
175 if (enableLongPressServiceHint) {
176 document.addEventListener('keydown', e => {
177 this.handleShortcutIndex(e);
178 });
179
180 document.addEventListener('keyup', e => {
181 this.handleShortcutIndex(e, false);
182 });
183 }
184 };
185
186 componentDidMount() {
187 const { service, stores } = this.props;
188
189 if (IS_SERVICE_DEBUGGING_ENABLED) {
190 autorun(() => {
191 if (Date.now() - service.lastPoll < ms('0.2s')) {
192 this.isPolled = true;
193
194 setTimeout(() => {
195 this.isPolled = false;
196 }, ms('1s'));
197 }
198
199 if (Date.now() - service.lastPollAnswer < ms('0.2s')) {
200 this.isPollAnswered = true;
201
202 setTimeout(() => {
203 this.isPollAnswered = false;
204 }, ms('1s'));
205 }
206 });
207 }
208
209 this.checkForLongPress(stores!.settings.app.enableLongPressServiceHint);
210 }
211
212 render() {
213 const {
214 classes,
215 service,
216 clickHandler,
217 shortcutIndex,
218 reload,
219 toggleNotifications,
220 toggleAudio,
221 toggleDarkMode,
222 deleteService,
223 disableService,
224 enableService,
225 hibernateService,
226 wakeUpService,
227 openSettings,
228 showMessageBadgeWhenMutedSetting,
229 showServiceNameSetting,
230 showMessageBadgesEvenWhenMuted,
231 } = this.props;
232 const { intl } = this.props;
233
234 const menuTemplate: Array<MenuItemConstructorOptions> = [
235 {
236 label: service.name || service.recipe.name,
237 enabled: false,
238 },
239 {
240 type: 'separator',
241 },
242 {
243 label: intl.formatMessage(messages.reload),
244 click: reload,
245 accelerator: `${cmdOrCtrlShortcutKey()}+R`,
246 enabled: service.isEnabled,
247 },
248 {
249 label: intl.formatMessage(globalMessages.edit),
250 click: () =>
251 openSettings({
252 path: `services/edit/${service.id}`,
253 }),
254 },
255 {
256 type: 'separator',
257 },
258 {
259 label: service.isNotificationEnabled
260 ? intl.formatMessage(messages.disableNotifications)
261 : intl.formatMessage(messages.enableNotifications),
262 click: () => toggleNotifications(),
263 accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+N`,
264 enabled: service.isEnabled,
265 },
266 {
267 label: service.isMuted
268 ? intl.formatMessage(messages.enableAudio)
269 : intl.formatMessage(messages.disableAudio),
270 click: () => toggleAudio(),
271 accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+A`,
272 enabled: service.isEnabled,
273 },
274 {
275 label: service.isDarkModeEnabled
276 ? intl.formatMessage(messages.disableDarkMode)
277 : intl.formatMessage(messages.enableDarkMode),
278 click: () => toggleDarkMode(),
279 accelerator: `${shiftKey()}+${altKey()}+D`,
280 enabled: service.isEnabled,
281 },
282 {
283 label: intl.formatMessage(
284 service.isEnabled ? messages.disableService : messages.enableService,
285 ),
286 click: () => (service.isEnabled ? disableService() : enableService()),
287 accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+S`,
288 },
289 {
290 label: intl.formatMessage(
291 service.isHibernating
292 ? messages.wakeUpService
293 : messages.hibernateService,
294 ),
295 // eslint-disable-next-line no-confusing-arrow
296 click: () =>
297 service.isHibernating ? wakeUpService() : hibernateService(),
298 enabled: service.isEnabled && service.canHibernate,
299 },
300 {
301 type: 'separator',
302 },
303 {
304 label: intl.formatMessage(messages.deleteService),
305 click: () => {
306 // @ts-ignore
307 const selection = dialog.showMessageBoxSync(app.mainWindow, {
308 type: 'question',
309 message: intl.formatMessage(messages.deleteService),
310 detail: intl.formatMessage(messages.confirmDeleteService, {
311 serviceName: service.name || service.recipe.name,
312 }),
313 buttons: [
314 intl.formatMessage(globalMessages.yes),
315 intl.formatMessage(globalMessages.no),
316 ],
317 });
318 if (selection === 0) {
319 deleteService();
320 }
321 },
322 },
323 ];
324 const menu = Menu.buildFromTemplate(menuTemplate);
325
326 const showNotificationBadge =
327 (showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) &&
328 showMessageBadgesEvenWhenMuted &&
329 service.isBadgeEnabled;
330
331 const showMediaBadge =
332 service.isMediaBadgeEnabled &&
333 service.isMediaPlaying &&
334 service.isEnabled;
335 const mediaBadge = (
336 <Icon icon={mdiVolumeSource} className="tab-item__icon" />
337 );
338
339 return (
340 <li
341 className={classnames({
342 [classes.stale]:
343 IS_SERVICE_DEBUGGING_ENABLED && service.lostRecipeConnection,
344 'tab-item': true,
345 'is-active': service.isActive,
346 'has-custom-icon': service.hasCustomIcon,
347 'is-disabled': !service.isEnabled,
348 'is-label-enabled': showServiceNameSetting,
349 })}
350 onClick={clickHandler}
351 onContextMenu={() => menu.popup()}
352 data-tip={`${service.name} ${
353 shortcutIndex <= 9
354 ? `(${cmdOrCtrlShortcutKey(false)}+${shortcutIndex})`
355 : ''
356 }`}
357 >
358 <img src={service.icon} className="tab-item__icon" alt="" />
359 {showServiceNameSetting && (
360 <span className="tab-item__label">{service.name}</span>
361 )}
362 {showNotificationBadge && (
363 <>
364 {service.unreadDirectMessageCount > 0 && (
365 <span className="tab-item__message-count">
366 {service.unreadDirectMessageCount}
367 </span>
368 )}
369 {service.unreadIndirectMessageCount > 0 &&
370 service.unreadDirectMessageCount === 0 &&
371 service.isIndirectMessageBadgeEnabled && (
372 <span className="tab-item__message-count is-indirect">•</span>
373 )}
374 {service.isHibernating && (
375 <span className="tab-item__message-count hibernating">•</span>
376 )}
377 </>
378 )}
379 {service.isError && (
380 <Icon icon={mdiExclamation} className="tab-item__error-icon" />
381 )}
382 {showMediaBadge && mediaBadge}
383 {IS_SERVICE_DEBUGGING_ENABLED && (
384 <>
385 <div
386 className={classnames({
387 [classes.pollIndicator]: true,
388 [classes.pollIndicatorPoll]: true,
389 [classes.polled]: this.isPolled,
390 })}
391 />
392 <div
393 className={classnames({
394 [classes.pollIndicator]: true,
395 [classes.pollIndicatorAnswer]: true,
396 [classes.pollAnswered]: this.isPollAnswered,
397 })}
398 />
399 </>
400 )}
401 {shortcutIndex <= 9 && this.state.showShortcutIndex && (
402 <span className="tab-item__shortcut-index">{shortcutIndex}</span>
403 )}
404 </li>
405 );
406 }
407}
408
409export default injectIntl(
410 SortableElement(injectSheet(styles, { injectTheme: true })(TabItem)),
411);