diff options
Diffstat (limited to 'src/components/services/tabs/TabItem.tsx')
-rw-r--r-- | src/components/services/tabs/TabItem.tsx | 411 |
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 @@ | |||
1 | import { app, dialog, Menu } from '@electron/remote'; | ||
2 | import { Component } from 'react'; | ||
3 | import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; | ||
4 | import { inject, observer } from 'mobx-react'; | ||
5 | import classnames from 'classnames'; | ||
6 | import { SortableElement } from 'react-sortable-hoc'; | ||
7 | import injectSheet, { WithStylesProps } from 'react-jss'; | ||
8 | import ms from 'ms'; | ||
9 | |||
10 | import { autorun, makeObservable, observable, reaction } from 'mobx'; | ||
11 | import { mdiExclamation, mdiVolumeSource } from '@mdi/js'; | ||
12 | import Service from '../../../models/Service'; | ||
13 | import { altKey, cmdOrCtrlShortcutKey, shiftKey } from '../../../environment'; | ||
14 | import globalMessages from '../../../i18n/globalMessages'; | ||
15 | import Icon from '../../ui/icon'; | ||
16 | import { Stores } from '../../../@types/stores.types'; | ||
17 | import MenuItemConstructorOptions = Electron.MenuItemConstructorOptions; | ||
18 | |||
19 | const IS_SERVICE_DEBUGGING_ENABLED = ( | ||
20 | localStorage.getItem('debug') || '' | ||
21 | ).includes('Ferdium:Service'); | ||
22 | |||
23 | const 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 | |||
78 | let pollIndicatorTransition = 'none'; | ||
79 | let polledTransition = 'none'; | ||
80 | let pollAnsweredTransition = 'none'; | ||
81 | |||
82 | if (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 | |||
88 | const 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 | |||
117 | interface 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 | |||
138 | interface IState { | ||
139 | showShortcutIndex: boolean; | ||
140 | } | ||
141 | |||
142 | @inject('stores') | ||
143 | @observer | ||
144 | class 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 | |||
409 | export default injectIntl( | ||
410 | SortableElement(injectSheet(styles, { injectTheme: true })(TabItem)), | ||
411 | ); | ||