Skip to content

Commit 20e4f12

Browse files
committed
dnd: Add feature to enable time based DND Mode
Create a menu to allow selecting time for which DND will be enabled. DND will go off at the scheduled time and a toast will be shown, both while setting the DND, thus conveying the DND Off time and again at the time when DND goes off. 'Until I resume' option is time independent, trivially. Fixes #561
1 parent b6f4e2b commit 20e4f12

File tree

5 files changed

+266
-3
lines changed

5 files changed

+266
-3
lines changed

app/renderer/css/main.css

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,70 @@ body {
8282
flex-direction: column;
8383
}
8484

85+
86+
/* DND Menu */
87+
88+
@keyframes fading {
89+
0% {
90+
opacity: 0;
91+
}
92+
20% {
93+
opacity: 1;
94+
}
95+
80% {
96+
opacity: 1;
97+
}
98+
100% {
99+
opacity: 0;
100+
}
101+
}
102+
103+
.toast {
104+
font-family: Verdana;
105+
border-radius: 10px;
106+
font-size: 13.5px;
107+
left: 45%;
108+
top: 10px;
109+
position: fixed;
110+
background-color: rgba(200, 200, 200, 1.000);
111+
padding: 15px 30px 15px 30px;
112+
color: rgb(0, 0, 0);
113+
animation: fading 4s;
114+
}
115+
116+
.dnd-menu {
117+
font-family: Verdana;
118+
font-size: 13px;
119+
left: 60px;
120+
bottom: 10px;
121+
position: fixed;
122+
background-color: transparent;
123+
padding: 10px;
124+
width: 150px;
125+
min-height: 150px;
126+
color: rgb(255, 255, 255);
127+
list-style-type: none;
128+
}
129+
130+
.dnd-time-btn {
131+
background-color: rgba(34, 44, 49, 1.000);
132+
border-style: solid;
133+
border-color: rgba(255, 255, 255, 1.000);
134+
border-width: 1.5px;
135+
border-radius: 20px;
136+
padding: 10px;
137+
text-align: center;
138+
margin: 2px 0;
139+
transition: .1s;
140+
}
141+
142+
.dnd-time-btn:hover {
143+
background-color: rgba(59, 76, 85, 1.000);
144+
transition: .2s;
145+
cursor: pointer;
146+
}
147+
148+
85149
.material-icons {
86150
font-family: 'Material Icons';
87151
font-weight: normal;

app/renderer/js/main.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,15 @@ class ServerManagerView {
403403

404404
initLeftSidebarEvents(): void {
405405
this.$dndButton.addEventListener('click', () => {
406-
const dndUtil = DNDUtil.toggle();
407-
ipcRenderer.send('forward-message', 'toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
406+
const dnd = ConfigUtil.getConfigItem('dnd', false);
407+
if (dnd) {
408+
const dndUtil = DNDUtil.toggle();
409+
ipcRenderer.send('forward-message', 'toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
410+
} else if (document.querySelector('.dnd-menu') === null) {
411+
DNDUtil.makeMenu();
412+
} else {
413+
document.querySelector('.dnd-menu').remove();
414+
}
408415
});
409416
this.$reloadButton.addEventListener('click', () => {
410417
this.tabs[this.activeTabIndex].webview.reload();
@@ -428,6 +435,7 @@ class ServerManagerView {
428435
}
429436

430437
initDNDButton(): void {
438+
DNDUtil.setDNDTimer();
431439
const dnd = ConfigUtil.getConfigItem('dnd', false);
432440
this.toggleDNDButton(dnd);
433441
}

app/renderer/js/utils/dnd-util.ts

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as ConfigUtil from './config-util';
2-
2+
import {ipcRenderer} from 'electron';
3+
import schedule from 'node-schedule';
34
type SettingName = 'showNotification' | 'silent' | 'flashTaskbarOnMessage';
45

56
export interface DNDSettings {
@@ -13,6 +14,10 @@ interface Toggle {
1314
newSettings: DNDSettings;
1415
}
1516

17+
/* Node-schedule job instance accepts time parameter and schedules to turn off DND
18+
and pops a toast that DND has ended. Cancel a pre-existing job before scheduling a new one. */
19+
let job: schedule.Job = null;
20+
1621
export function toggle(): Toggle {
1722
const dnd = !ConfigUtil.getConfigItem('dnd', false);
1823
const dndSettingList: SettingName[] = ['showNotification', 'silent'];
@@ -37,6 +42,10 @@ export function toggle(): Toggle {
3742
ConfigUtil.setConfigItem('dndPreviousSettings', oldSettings);
3843
} else {
3944
newSettings = ConfigUtil.getConfigItem('dndPreviousSettings');
45+
ConfigUtil.setConfigItem('dndSwitchOff', null);
46+
if (job !== null) {
47+
job.cancel();
48+
}
4049
}
4150

4251
for (const settingName of dndSettingList) {
@@ -46,3 +55,125 @@ export function toggle(): Toggle {
4655
ConfigUtil.setConfigItem('dnd', dnd);
4756
return {dnd, newSettings};
4857
}
58+
59+
/* Create the DND menu and closed it when focussed out. */
60+
export const makeMenu = (): void => {
61+
const actionsContainer = document.querySelector('#actions-container');
62+
const dndExist = document.querySelectorAll('.dnd-menu');
63+
if (dndExist.length === 0) {
64+
const dndMenu = document.createElement('ul');
65+
dndMenu.className = 'dnd-menu';
66+
const dndOptions: {[key: string]: string} = {
67+
'30 Minutes': 'thirty_min',
68+
'1 Hour': 'one_hr',
69+
'6 Hours': 'six_hr',
70+
'12 Hours': 'twelve_hr',
71+
'Until I Resume': 'custom'
72+
};
73+
74+
Object.keys(dndOptions).forEach(key => {
75+
const opt = document.createElement('li');
76+
opt.className = 'dnd-time-btn';
77+
opt.innerHTML = key;
78+
opt.id = dndOptions[key];
79+
opt.addEventListener('click', () => {
80+
configureDND(opt, dndMenu);
81+
}, false);
82+
dndMenu.append(opt);
83+
});
84+
actionsContainer.append(dndMenu);
85+
86+
const dndMenuFocusOut = (event: Event) => {
87+
console.log(event);
88+
if ((event.target as HTMLLIElement).innerHTML !== 'notifications') {
89+
dndMenu.remove();
90+
document.removeEventListener('click', dndMenuFocusOut);
91+
}
92+
};
93+
94+
document.addEventListener('keydown', event => {
95+
if (event.key === 'Escape') {
96+
dndMenu.remove();
97+
document.removeEventListener('click', dndMenuFocusOut);
98+
}
99+
});
100+
document.addEventListener('click', dndMenuFocusOut);
101+
}
102+
};
103+
104+
/* Configure DND as per user's selected option and pops a toast showing dnd switch off time. */
105+
const configureDND = (opt: HTMLLIElement, dndMenu: HTMLUListElement): void => {
106+
let dndOffTime;
107+
dndOffTime = new Date();
108+
const optionElement = document.querySelector('#' + opt.id);
109+
dndMenu.style.height = '75px';
110+
optionElement.className = 'dnd-options-btn';
111+
switch (opt.id) {
112+
case 'thirty_min':
113+
dndOffTime.setMinutes(dndOffTime.getMinutes() + 30);
114+
break;
115+
case 'one_hr':
116+
dndOffTime.setMinutes(dndOffTime.getMinutes() + 60);
117+
break;
118+
case 'six_hr':
119+
dndOffTime.setMinutes(dndOffTime.getMinutes() + 360);
120+
break;
121+
case 'twelve_hr':
122+
dndOffTime.setMinutes(dndOffTime.getMinutes() + 720);
123+
break;
124+
default:
125+
dndOffTime = null;
126+
break;
127+
}
128+
129+
ConfigUtil.setConfigItem('dndSwitchOff', dndOffTime);
130+
131+
dndMenu.remove();
132+
showDNDTimeLeft();
133+
setDNDTimer();
134+
const state = toggle();
135+
ipcRenderer.send('forward-message', 'toggle-dnd', state.dnd, state.newSettings);
136+
};
137+
138+
/* Fetch DND switch off time and toggle DND off if the time has elapsed. */
139+
export const setDNDTimer = (): void => {
140+
const dbTime = ConfigUtil.getConfigItem('dndSwitchOff');
141+
if (dbTime !== null) {
142+
const time = new Date(dbTime);
143+
// Handle the case when user closes the app before switch off time and stars it after the time has elapsed.
144+
if (time < new Date()) {
145+
toggle();
146+
return;
147+
}
148+
149+
if (job !== null) {
150+
job.cancel();
151+
}
152+
153+
job = schedule.scheduleJob(time, () => {
154+
ConfigUtil.setConfigItem('dndSwitchOff', null);
155+
const state = toggle();
156+
ipcRenderer.send('forward-message', 'toggle-dnd', state.dnd, state.newSettings);
157+
showToast('DND has ended');
158+
});
159+
}
160+
};
161+
162+
/* Show a toast with the clock time when DND will go off. */
163+
export const showDNDTimeLeft = (): void => {
164+
const check = ConfigUtil.getConfigItem('dndSwitchOff');
165+
if (check !== null) {
166+
const dbTime = new Date(check);
167+
const timeLeft = `DND goes off at ${dbTime.getHours()} : ${(dbTime.getMinutes() < 10 ? (`0${dbTime.getMinutes()}`) : dbTime.getMinutes())}`;
168+
showToast(timeLeft);
169+
}
170+
};
171+
172+
export const showToast = (t: string): void => {
173+
const actionsContainer = document.querySelector('#actions-container');
174+
const toast = document.createElement('p');
175+
toast.className = 'toast';
176+
toast.innerHTML = t;
177+
actionsContainer.append(toast);
178+
setTimeout(() => toast.remove(), 4000);
179+
};

package-lock.json

Lines changed: 58 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
"dependencies": {
149149
"@electron-elements/send-feedback": "^2.0.3",
150150
"@sentry/electron": "^1.3.0",
151+
"@types/node-schedule": "^1.3.0",
151152
"adm-zip": "^0.4.14",
152153
"auto-launch": "^5.0.5",
153154
"backoff": "^2.5.0",
@@ -160,6 +161,7 @@
160161
"fs-extra": "^9.0.0",
161162
"i18n": "^0.9.1",
162163
"node-json-db": "^1.1.0",
164+
"node-schedule": "^1.3.2",
163165
"request": "^2.88.2",
164166
"rxjs": "^5.5.12",
165167
"semver": "^7.3.2"

0 commit comments

Comments
 (0)