Skip to content

Feature/dnd duration dropdown #1419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/common/config-schemata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const dndSettingsSchemata = {

export const configSchemata = {
...dndSettingsSchemata,
dndExpiration: z.number().nullable().default(null),
appLanguage: z.string().nullable(),
autoHideMenubar: z.boolean(),
autoUpdate: z.boolean(),
Expand Down
7 changes: 6 additions & 1 deletion app/common/typed-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ export type RendererMessage = {
autoHideMenubar: boolean,
updateMenu: boolean,
) => void;
"toggle-dnd": (state: boolean, newSettings: Partial<DndSettings>) => void;
"toggle-dnd-request": (duration?: number) => void;
"toggle-dnd": (
state: boolean,
newSettings: Partial<DndSettings>,
duration?: number,
) => void;
"toggle-sidebar": (show: boolean) => void;
"toggle-silent": (state: boolean) => void;
"toggle-tray": (state: boolean) => void;
Expand Down
49 changes: 48 additions & 1 deletion app/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as remoteMain from "@electron/remote/main";
import windowStateKeeper from "electron-window-state";

import * as ConfigUtil from "../common/config-util.js";
import * as DNDUtil from "../common/dnd-util.js";
import {bundlePath, bundleUrl, publicPath} from "../common/paths.js";
import * as t from "../common/translation-util.js";
import type {RendererMessage} from "../common/typed-ipc.js";
Expand Down Expand Up @@ -48,6 +49,7 @@ let badgeCount: number;

let isQuitting = false;

let dndRevertTimeout: NodeJS.Timeout | null = null;
// Load this file in main window
const mainUrl = new URL("app/renderer/main.html", bundleUrl).href;

Expand All @@ -68,6 +70,23 @@ const toggleApp = (): void => {
}
};

function checkDndExpirationOnStartup(page: WebContents): void {
const expiration = ConfigUtil.getConfigItem("dndExpiration", null);

if (expiration && Date.now() > expiration) {
const revert = DNDUtil.toggle();
send(page, "toggle-dnd", revert.dnd, revert.newSettings);
ConfigUtil.removeConfigItem("dndExpiration");
} else if (expiration) {
const timeLeft = expiration - Date.now();
dndRevertTimeout = setTimeout(() => {
const revert = DNDUtil.toggle();
send(page, "toggle-dnd", revert.dnd, revert.newSettings);
ConfigUtil.removeConfigItem("dndExpiration");
}, timeLeft);
}
}

function createMainWindow(): BrowserWindow {
// Load the previous state with fallback to defaults
mainWindowState = windowStateKeeper({
Expand Down Expand Up @@ -270,7 +289,7 @@ function createMainWindow(): BrowserWindow {
}

const page = mainWindow.webContents;

checkDndExpirationOnStartup(page);
page.on("dom-ready", () => {
if (ConfigUtil.getConfigItem("startMinimized", false)) {
mainWindow.hide();
Expand Down Expand Up @@ -407,6 +426,34 @@ function createMainWindow(): BrowserWindow {
listener: Channel,
...parameters: Parameters<RendererMessage[Channel]>
) => {
if (listener === "toggle-dnd-request") {
const [duration] = parameters as [number?];
const result = DNDUtil.toggle();
send(_event.sender, "toggle-dnd", result.dnd, result.newSettings);

if (result.dnd && duration && !Number.isNaN(duration)) {
if (dndRevertTimeout) clearTimeout(dndRevertTimeout);

const expirationTime = Date.now() + duration * 60 * 1000;
ConfigUtil.setConfigItem("dndExpiration", expirationTime);

dndRevertTimeout = setTimeout(
() => {
const revert = DNDUtil.toggle();
send(_event.sender, "toggle-dnd", revert.dnd, revert.newSettings);
ConfigUtil.removeConfigItem("dndExpiration");
},
duration * 60 * 1000,
);
} else if (dndRevertTimeout) {
clearTimeout(dndRevertTimeout);
dndRevertTimeout = null;
ConfigUtil.removeConfigItem("dndExpiration");
}

return;
}

send(page, listener, ...parameters);
},
);
Expand Down
25 changes: 25 additions & 0 deletions app/renderer/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,31 @@ webview.focus {
font-size: 14px;
}

.dnd-dropdown {
position: absolute;
left: 60px;
background-color: rgb(32 33 36 / 100%); /* closer to Zulip's sidebars */
border: 1px solid rgb(60 60 60 / 100%);
border-radius: 8px;
color: rgb(240 240 240 / 100%);
z-index: 1000;
padding: 4px 0;
font-size: 13px;
font-family: arial, sans-serif;
min-width: 150px;
box-shadow: 0 4px 12px rgb(0 0 0 / 40%);
}

.dnd-dropdown-item {
padding: 8px 16px;
cursor: pointer;
transition: background-color 0.15s ease;
}

.dnd-dropdown-item:hover {
background-color: rgb(60 60 60 / 100%);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not well-scoped CSS; you don't want to apply CSS to entire rules like div for readability and fragility reasons.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled this by applying style to specific class.


#loading-tooltip::after,
#dnd-tooltip::after,
#back-tooltip::after,
Expand Down
44 changes: 36 additions & 8 deletions app/renderer/js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import * as Sentry from "@sentry/electron/renderer";

import type {Config} from "../../common/config-util.js";
import * as ConfigUtil from "../../common/config-util.js";
import * as DNDUtil from "../../common/dnd-util.js";
import type {DndSettings} from "../../common/dnd-util.js";
import * as EnterpriseUtil from "../../common/enterprise-util.js";
import {html} from "../../common/html.js";
Expand Down Expand Up @@ -444,14 +443,34 @@ export class ServerManagerView {

initLeftSidebarEvents(): void {
this.$dndButton.addEventListener("click", () => {
const dndUtil = DNDUtil.toggle();
ipcRenderer.send(
"forward-message",
"toggle-dnd",
dndUtil.dnd,
dndUtil.newSettings,
);
const isDndOn = ConfigUtil.getConfigItem("dnd", false);
if (isDndOn) {
ipcRenderer.send("forward-message", "toggle-dnd-request", undefined);
return;
}

const dropdown = document.querySelector("#dnd-dropdown");
dropdown?.classList.toggle("hidden");
this.$dndTooltip.classList.add("hidden");
dropdown?.addEventListener("mouseleave", () => {
dropdown.classList.add("hidden");
this.$dndTooltip.classList.remove("hidden");
});
});
const dropdownItems = document.querySelectorAll("#dnd-dropdown div");
for (const item of dropdownItems) {
item.addEventListener("click", (event) => {
const target = event.target as HTMLElement;
const value = target.dataset.minutes;
const duration = value === "forever" ? undefined : Number(value);

ipcRenderer.send("forward-message", "toggle-dnd-request", duration);

document.querySelector("#dnd-dropdown")?.classList.add("hidden");
this.$dndTooltip.classList.remove("hidden");
});
}

this.$reloadButton.addEventListener("click", async () => {
const tab = this.tabs[this.activeTabIndex];
if (tab instanceof ServerTab) (await tab.webview).reload();
Expand Down Expand Up @@ -1210,6 +1229,15 @@ window.addEventListener("load", async () => {
>${t.__("Do Not Disturb")}</span
>
</div>
<div id="dnd-dropdown" class="dnd-dropdown hidden">
<div class="dnd-dropdown-item" data-minutes="30">30 minutes</div>
<div class="dnd-dropdown-item" data-minutes="60">1 hour</div>
<div class="dnd-dropdown-item" data-minutes="180">3 hours</div>
<div class="dnd-dropdown-item" data-minutes="720">12 hours</div>
<div class="dnd-dropdown-item" data-minutes="forever">
Until I resume
</div>
</div>
<div class="action-button hidden" id="reload-action">
<i class="material-icons md-48">refresh</i>
<span id="reload-tooltip" style="display: none"
Expand Down