From ac8c696dc820e185b755ffe798b30505164afd90 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:32:47 -0400 Subject: [PATCH] KeywordNotify & NotificationApi Updates --- .../Notifications/NotificationComponent.tsx | 30 ++- src/api/Notifications/Notifications.tsx | 30 ++- src/api/Settings.ts | 2 + .../tabs/vencord/NotificationSettings.tsx | 10 +- src/equicordplugins/keywordNotify/index.tsx | 180 +++++++++++------- src/plugins/_core/settings.tsx | 2 +- 6 files changed, 170 insertions(+), 84 deletions(-) diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx index 52e56d5d0..e8f0cc206 100644 --- a/src/api/Notifications/NotificationComponent.tsx +++ b/src/api/Notifications/NotificationComponent.tsx @@ -21,7 +21,7 @@ import "./styles.css"; import { useSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { classes } from "@utils/misc"; -import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common"; +import { React, useEffect, useRef, useState } from "@webpack/common"; import { NotificationData } from "./Notifications"; @@ -39,26 +39,38 @@ export default ErrorBoundary.wrap(function NotificationComponent({ dismissOnClick }: NotificationData & { className?: string; }) { const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications; - const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused()); const [isHover, setIsHover] = useState(false); const [elapsed, setElapsed] = useState(0); - const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]); + const start = useRef(Date.now()); + const pause = useRef(null); useEffect(() => { - if (isHover || !hasFocus || timeout === 0 || permanent) return void setElapsed(0); + if (timeout === 0 || permanent) return; + + if (isHover) { + if (pause.current === null) pause.current = Date.now(); + return; + } + + if (pause.current !== null) { + const pausedFor = Date.now() - pause.current; + start.current += pausedFor; + pause.current = null; + } const intervalId = setInterval(() => { - const elapsed = Date.now() - start; - if (elapsed >= timeout) + const elapsedNow = Date.now() - start.current; + if (elapsedNow >= timeout) { onClose!(); - else - setElapsed(elapsed); + } else { + setElapsed(elapsedNow); + } }, 10); return () => clearInterval(intervalId); - }, [timeout, isHover, hasFocus]); + }, [timeout, isHover, permanent]); const timeoutProgress = elapsed / timeout; diff --git a/src/api/Notifications/Notifications.tsx b/src/api/Notifications/Notifications.tsx index 96a3425b5..4fd8c3d93 100644 --- a/src/api/Notifications/Notifications.tsx +++ b/src/api/Notifications/Notifications.tsx @@ -18,17 +18,31 @@ import { Settings } from "@api/Settings"; import { Queue } from "@utils/Queue"; -import { createRoot } from "@webpack/common"; +import { createRoot, WindowStore } from "@webpack/common"; import type { ReactNode } from "react"; import type { Root } from "react-dom/client"; import NotificationComponent from "./NotificationComponent"; -import { persistNotification } from "./notificationLog"; +import { openNotificationLogModal, persistNotification } from "./notificationLog"; const NotificationQueue = new Queue(); let reactRoot: Root; let id = 42; +let missedCount = 0; + +window.addEventListener("focus", () => { + const { missed, timeout, useNative } = Settings.notifications; + if (!missed) return; + if (missedCount > 0 && timeout > 0 && useNative === "never") { + showNotification({ + title: "While you were away", + body: `${missedCount} notifications received`, + onClick: () => openNotificationLogModal(), + }); + missedCount = 0; + } +}); function getRoot() { if (!reactRoot) { @@ -105,7 +119,17 @@ export async function showNotification(data: NotificationData) { }); n.onclick = onClick; n.onclose = onClose; + + if (!WindowStore.isFocused()) missedCount++; } else { - NotificationQueue.push(() => _showNotification(data, id++)); + NotificationQueue.push(() => + _showNotification({ + ...data, + onClose: () => { + data.onClose?.(); + if (!WindowStore.isFocused()) missedCount++; + } + }, id++) + ); } } diff --git a/src/api/Settings.ts b/src/api/Settings.ts index ddf326b01..d4c64647f 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -68,6 +68,7 @@ export interface Settings { timeout: number; position: "top-right" | "bottom-right"; useNative: "always" | "never" | "not-focused"; + missed: boolean; logLimit: number; }; @@ -109,6 +110,7 @@ const DefaultSettings: Settings = { timeout: 5000, position: "bottom-right", useNative: "not-focused", + missed: true, logLimit: 50 }, diff --git a/src/components/settings/tabs/vencord/NotificationSettings.tsx b/src/components/settings/tabs/vencord/NotificationSettings.tsx index 95e6760d6..65057c6d1 100644 --- a/src/components/settings/tabs/vencord/NotificationSettings.tsx +++ b/src/components/settings/tabs/vencord/NotificationSettings.tsx @@ -11,7 +11,7 @@ import { Flex } from "@components/Flex"; import { Margins } from "@utils/margins"; import { identity } from "@utils/misc"; import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { Button, Forms, Select, Slider, Text } from "@webpack/common"; +import { Button, Forms, Select, Slider, Switch, Text } from "@webpack/common"; export function NotificationSection() { return ( @@ -88,6 +88,14 @@ function NotificationSettings() { serialize={identity} /> + Missed Notification Count + settings.missed = v} + > + When refocusing discord a notification will popup with how you missed + + Notification Timeout Set to 0s to never automatically time out , listType: ListType, ignoreCase: boolean; }; let keywordEntries: Array = []; -let currentUser: User; let keywordLog: Array = []; let interceptor: (e: any) => void; const recentMentionsPopoutClass = findByPropsLazy("recentMentionsPopout"); const tabClass = findByPropsLazy("inboxTitle", "tab"); const buttonClass = findByPropsLazy("size36"); -const MenuHeader = findByCodeLazy(".getUnseenInviteCount())"); const Popout = findByCodeLazy("getProTip", "canCloseAllMessages:"); const createMessageRecord = findByCodeLazy(".createFromServer(", ".isBlockedForMessage", "messageReference:"); const KEYWORD_ENTRIES_KEY = "KeywordNotify_keywordEntries"; @@ -85,9 +83,11 @@ function highlightKeywords(str: string, entries: Array) { const idx = str.indexOf(matches[0]); return [ - {str.substring(0, idx)}, - {matches[0]}, - {str.substring(idx + matches[0].length)} + <> + {str.substring(0, idx)}, + {matches[0]}, + {str.substring(idx + matches[0].length)} + ]; } @@ -102,7 +102,11 @@ function Collapsible({ title, children }) { size={Button.Sizes.ICON} className={cl("collapsible")}>
-
{isOpen ? "▼" : "▶"}
+
{isOpen ? "▼" : "▶"}
{title}
@@ -116,34 +120,36 @@ function ListedIds({ listIds, setListIds }) { const [values] = useState(listIds); async function onChange(e: string, index: number) { - values[index] = e; + values[index] = e.trim(); setListIds(values); update(); } const elements = values.map((currentValue: string, index: number) => { return ( - -
- onChange(e, index)} - /> -
- -
+ <> + +
+ onChange(e, index)} + /> +
+ +
+ ); }); @@ -192,6 +198,12 @@ function KeywordEntries() { update(); } + async function setIgnoreCase(index: number, value: boolean) { + keywordEntries[index].ignoreCase = value; + await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries); + update(); + } + const elements = keywordEntries.map((entry, i) => { return ( <> @@ -216,8 +228,7 @@ function KeywordEntries() { { - values[i].ignoreCase = !values[i].ignoreCase; - update(); + setIgnoreCase(i, !values[i].ignoreCase); }} style={{ marginTop: "0.5em", marginRight: "40px" }} > @@ -314,16 +325,22 @@ export default definePlugin({ patches: [ { find: "#{intl::UNREADS_TAB_LABEL})}", - replacement: { - match: /,(\i\?\(0,\i\.jsxs\)\(\i\.\i\i\.Item)/, - replace: ",$self.keywordTabBar()$&" - } + replacement: [ + { + match: /,(\i\?\(0,\i\.jsxs\)\(\i\.\i\i\.Item)/, + replace: ",$self.keywordTabBar()$&" + }, + { + match: /:(\i)===\i\.\i\.MENTIONS\?/, + replace: ": $1 === 8 ? $self.keywordClearButton() $&" + } + ] }, { find: "location:\"RecentsPopout\"});", replacement: { - match: /(?<=setTab:(\i),badgeState:\i,closePopout:(\i).{0,50}):(\i)===\i\.\i\.MENTIONS\?\(0,.+?onJump:(\i)/, - replace: ": $3 === 8 ? $self.tryKeywordMenu($1, $4, $2) $&" + match: /:(\i)===\i\.\i\.MENTIONS\?\(0,.+?onJump:(\i)}\)/, + replace: ": $1 === 8 ? $self.tryKeywordMenu($2) $&" } }, { @@ -343,11 +360,14 @@ export default definePlugin({ async start() { this.onUpdate = () => null; - currentUser = UserStore.getCurrentUser(); keywordEntries = await DataStore.get(KEYWORD_ENTRIES_KEY) ?? []; await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries); (await DataStore.get(KEYWORD_LOG_KEY) ?? []).map(e => JSON.parse(e)).forEach(e => { - this.addToLog(e); + try { + this.addToLog(e); + } catch (err) { + console.error(err); + } }); interceptor = (e: any) => { @@ -371,11 +391,11 @@ export default definePlugin({ continue; } - let listed = entry.listIds.some(id => id === m.channel_id || id === m.author.id); + let listed = entry.listIds.some(id => id.trim() === m.channel_id || id === m.author.id); if (!listed) { const channel = ChannelStore.getChannel(m.channel_id); if (channel != null) { - listed = entry.listIds.some(id => id === channel.guild_id); + listed = entry.listIds.some(id => id.trim() === channel.guild_id); } } @@ -413,24 +433,45 @@ export default definePlugin({ } if (matches) { - // @ts-ignore - m.mentions.push({ id: currentUser.id }); + const id = UserStore.getCurrentUser()?.id; + if (id !== null) { + // @ts-ignore + m.mentions.push({ id: id }); + } - if (m.author.id !== currentUser.id) + if (m.author.id !== id) { + this.storeMessage(m); this.addToLog(m); + } } }, + storeMessage(m: Message) { + if (m == null) + return; + DataStore.get(KEYWORD_LOG_KEY).then(log => { + log = log ? log.map((e: string) => JSON.parse(e)) : []; + + log.push(m); + if (log.length > settings.store.amountToKeep) { + log = log.slice(-settings.store.amountToKeep); + } + + DataStore.set(KEYWORD_LOG_KEY, log.map(e => JSON.stringify(e))); + }); + }, addToLog(m: Message) { if (m == null || keywordLog.some(e => e.id === m.id)) return; - DataStore.get(KEYWORD_LOG_KEY).then(log => { - DataStore.set(KEYWORD_LOG_KEY, [...log, JSON.stringify(m)]); - }); + let messageRecord: any; + try { + messageRecord = createMessageRecord(m); + } catch (err) { + return; + } - const thing = createMessageRecord(m); - keywordLog.push(thing); + keywordLog.push(messageRecord); keywordLog.sort((a, b) => b.timestamp - a.timestamp); while (keywordLog.length > settings.store.amountToKeep) { @@ -453,28 +494,27 @@ export default definePlugin({ ); }, - tryKeywordMenu(setTab, onJump, closePopout) { - const header = ( - <> - - - {({ onMouseLeave, onMouseEnter }) => ( -
{ - keywordLog = []; - DataStore.set(KEYWORD_LOG_KEY, []); - this.onUpdate(); - }}> - -
- )} -
-
- + keywordClearButton() { + return ( + + {({ onMouseLeave, onMouseEnter }) => ( +
{ + keywordLog = []; + DataStore.set(KEYWORD_LOG_KEY, []); + this.onUpdate(); + }}> + +
+ )} +
); + }, + tryKeywordMenu(onJump) { const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId()); const [tempLogs, setKeywordLog] = useState(keywordLog); @@ -503,7 +543,7 @@ export default definePlugin({ <> header} + renderHeader={() => null} renderMessage={messageRender} channel={channel} onJump={onJump} diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 9633624d3..665687b39 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -40,7 +40,7 @@ export default definePlugin({ find: ".versionHash", replacement: [ { - match: /\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/, + match: /\.info.+?\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/, replace: (m, component, props) => { props = props.replace(/children:\[.+\]/, ""); return `${m},$self.makeInfoElements(${component}, ${props})`;