Plugin

BypassDND

Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.

index.tsx
Download

Source

src/plugins/bypassDND/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { type NavContextMenuPatchCallback } from "@api/ContextMenu";
8import { Notifications } from "@api/index";
9import { definePluginSettings } from "@api/Settings";
10import { Devs } from "@utils/constants";
11import { getCurrentChannel } from "@utils/discord";
12import { Logger } from "@utils/Logger";
13import definePlugin, { OptionType } from "@utils/types";
14import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore, WindowStore } from "@webpack/common";
15import type { Message, User as DiscordUser } from "discord-types/general";
16
17interface User extends DiscordUser {
18 globalName: string;
19}
20
21interface IMessageCreate {
22 channelId: string;
23 guildId: string;
24 message: Message;
25}
26
27function Icon(enabled?: boolean): JSX.Element {
28 return <svg
29 width="18"
30 height="18"
31 >
32 <circle cx="9" cy="9" r="8" fill={!enabled ? "var(--status-danger)" : "currentColor"} />
33 <circle cx="9" cy="9" r="3.75" fill={!enabled ? "white" : "black"} />
34 </svg>;
35}
36
37function processIds(value: string): string {
38 return value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", ");
39}
40
41async function showNotification(message: Message, guildId: string | undefined): Promise<void> {
42 const channel = ChannelStore.getChannel(message.channel_id);
43 const channelRegex = /<#(\d{19})>/g;
44 const userRegex = /<@(\d{18})>/g;
45
46 message.content = message.content.replace(channelRegex, (match, channelId: string) => {
47 return `#${ChannelStore.getChannel(channelId)?.name}`;
48 });
49
50 message.content = message.content.replace(userRegex, (match, userId: string) => {
51 return `@${(UserStore.getUser(userId) as User).globalName}`;
52 });
53
54 await Notifications.showNotification({
55 title: `${(message.author as User).globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`,
56 body: message.content,
57 icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false),
58 onClick: function (): void {
59 NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${message.channel_id}/${message.id}`);
60 }
61 });
62}
63
64function ContextCallback(name: "guild" | "user" | "channel"): NavContextMenuPatchCallback {
65 return (children, props) => {
66 const type = props[name];
67 if (!type) return;
68 const enabled = settings.store[`${name}s`].split(", ").includes(type.id);
69 if (name === "user" && type.id === UserStore.getCurrentUser().id) return;
70 children.splice(-1, 0, (
71 <Menu.MenuGroup>
72 <Menu.MenuItem
73 id={`dnd-${name}-bypass`}
74 label={`${enabled ? "Remove" : "Add"} DND Bypass`}
75 icon={() => Icon(enabled)}
76 action={() => {
77 let bypasses: string[] = settings.store[`${name}s`].split(", ");
78 if (enabled) bypasses = bypasses.filter(id => id !== type.id);
79 else bypasses.push(type.id);
80 settings.store[`${name}s`] = bypasses.filter(id => id.trim() !== "").join(", ");
81 }}
82 />
83 </Menu.MenuGroup>
84 ));
85 };
86}
87
88const settings = definePluginSettings({
89 guilds: {
90 type: OptionType.STRING,
91 description: "Guilds to let bypass (notified when pinged anywhere in guild)",
92 default: "",
93 placeholder: "Separate with commas",
94 onChange: value => settings.store.guilds = processIds(value)
95 },
96 channels: {
97 type: OptionType.STRING,
98 description: "Channels to let bypass (notified when pinged in that channel)",
99 default: "",
100 placeholder: "Separate with commas",
101 onChange: value => settings.store.channels = processIds(value)
102 },
103 users: {
104 type: OptionType.STRING,
105 description: "Users to let bypass (notified for all messages sent in DMs)",
106 default: "",
107 placeholder: "Separate with commas",
108 onChange: value => settings.store.users = processIds(value)
109 },
110 allowOutsideOfDms: {
111 type: OptionType.BOOLEAN,
112 description: "Allow selected users to bypass DND outside of DMs too (acts like a channel/guild bypass, but it&#039;s for all messages sent by the selected users)"
113 }
114});
115
116export default definePlugin({
117 name: "BypassDND",
118 description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.",
119 authors: [Devs.FiveCord],
120 flux: {
121 async MESSAGE_CREATE({ message, guildId, channelId }: IMessageCreate): Promise<void> {
122 try {
123 const currentUser = UserStore.getCurrentUser();
124 const userStatus = await PresenceStore.getStatus(currentUser.id);
125 const currentChannelId = getCurrentChannel()?.id ?? "0";
126 if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || (channelId === currentChannelId && WindowStore.isFocused()) || userStatus !== "dnd") {
127 return;
128 }
129 const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned;
130 if ((settings.store.guilds.split(", ").includes(guildId) || settings.store.channels.split(", ").includes(channelId)) && mentioned) {
131 await showNotification(message, guildId);
132 } else if (settings.store.users.split(", ").includes(message.author.id)) {
133 const userChannelId = await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id);
134 if (channelId === userChannelId || (mentioned && settings.store.allowOutsideOfDms === true)) {
135 await showNotification(message, guildId);
136 }
137 }
138 } catch (error) {
139 new Logger("BypassDND").error("Failed to handle message", error);
140 }
141 }
142 },
143 settings,
144 contextMenus: {
145 "guild-context": ContextCallback("guild"),
146 "channel-context": ContextCallback("channel"),
147 "user-context": ContextCallback("user"),
148 }
149});
150