From d695e3cac0d9a922800eca52ad26a68dcd2f2873 Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:32:18 +0100 Subject: [PATCH] update diff rendering logic (#419) * update diff to seperate removals from additions, removals on the first line greens on the 2nd line (being the current message) * Add an option to go back to the legacy diffs or to the current updated seperated diff logic * Update index.tsx * Quick Fix * Fixes * NewPluginsManager --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: justjxke --- .../newPluginsManager/NewPluginsModal.tsx | 205 ++++++++++++++++++ .../newPluginsManager/index.tsx | 28 +++ .../newPluginsManager/knownSettings.ts | 91 ++++++++ .../newPluginsManager/styles.css | 20 ++ src/plugins/messageLogger/index.tsx | 105 ++++++--- src/plugins/messageLogger/messageLogger.css | 12 + 6 files changed, 435 insertions(+), 26 deletions(-) create mode 100644 src/equicordplugins/newPluginsManager/NewPluginsModal.tsx create mode 100644 src/equicordplugins/newPluginsManager/index.tsx create mode 100644 src/equicordplugins/newPluginsManager/knownSettings.ts create mode 100644 src/equicordplugins/newPluginsManager/styles.css diff --git a/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx b/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx new file mode 100644 index 000000000..e6fa036da --- /dev/null +++ b/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx @@ -0,0 +1,205 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { Settings, useSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { PluginCard } from "@components/settings/tabs/plugins/PluginCard"; +import { ChangeList } from "@utils/ChangeList"; +import { classes, Margins } from "@utils/index"; +import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { useForceUpdater } from "@utils/react"; +import { findComponentByCodeLazy } from "@webpack"; +import { Button, Flex, Forms, React, Text, Tooltip, useMemo } from "@webpack/common"; +import { JSX } from "react"; + +import Plugins from "~plugins"; + +import { getNewPlugins, getNewSettings, KnownPluginSettingsMap, writeKnownSettings } from "./knownSettings"; + +const cl = classNameFactory("vc-plugins-"); + +const Checkbox = findComponentByCodeLazy(".checkboxWrapperDisabled:"); + +let hasSeen = false; + +// Most of this was stolen from PluginSettings directly. + +export function NewPluginsModal({ modalProps, newPlugins, newSettings }: { modalProps: ModalProps; newPlugins: Set; newSettings: KnownPluginSettingsMap; }) { + const settings = useSettings(); + const changes = React.useMemo(() => new ChangeList(), []); + let updateContinueButton = () => { }; + + const depMap = React.useMemo(() => { + const o = {} as Record; + for (const plugin in Plugins) { + const deps = Plugins[plugin].dependencies; + if (deps) { + for (const dep of deps) { + o[dep] ??= []; + o[dep].push(plugin); + } + } + } + return o; + }, []); + + const mapPlugins = (array: string[]) => array.map(pn => Plugins[pn]) + .sort((a, b) => a.name.localeCompare(b.name)); + + const sortedPlugins = useMemo(() => [ + ...mapPlugins([...newPlugins]), + ...mapPlugins([...newSettings.keys()].filter(p => !newPlugins.has(p))) + ], []); + + const plugins = [] as JSX.Element[]; + const requiredPlugins = [] as JSX.Element[]; + + for (const p of sortedPlugins) { + if (p.hidden) + continue; + + const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled); + + if (isRequired) { + const tooltipText = p.required + ? "This plugin is required for Vencord to function." + : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled)); + + requiredPlugins.push( + + {({ onMouseLeave, onMouseEnter }) => ( + { + changes.handleChange(name); + updateContinueButton(); + }} + disabled={true} + plugin={p} + key={p.name} + isNew={newPlugins.has(p.name)} + /> + )} + + ); + } else { + plugins.push( + { + changes.handleChange(name); + updateContinueButton(); + }} + disabled={false} + plugin={p} + key={p.name} + isNew={newPlugins.has(p.name)} + /> + ); + } + } + + + return + + New Plugins and Settings ({[...plugins, ...requiredPlugins].length}) + + {tooltipProps => +
+ +
+ } +
+
+ +
+ {[...plugins, ...requiredPlugins]} +
+
+ + + void) => updateContinueButton = v} + /> + + +
+ { + Settings.plugins.NewPluginsManager.enabled = !settings?.plugins?.NewPluginsManager?.enabled; + }} + > + Don't show this again + +
+
+
+
; +} + +function ContinueButton(props: { callback: (update: () => void) => void; changes: ChangeList; close: () => any; }) { + const update = useForceUpdater(); + props.callback(update); + return + The following plugins require a restart: +
+
    + {props.changes.map(p =>
  • {p}
  • )} +
+ } + shouldShow={props.changes.hasChanges} + > + {tooltipProps => + + } + ; +} + + +function makeDependencyList(deps: string[]) { + return ( + + This plugin is required by: + {deps.map((dep: string) => {dep})} + + ); +} + +export async function openNewPluginsModal() { + const newPlugins = await getNewPlugins(); + const newSettings = await getNewSettings(); + if ((newPlugins.size || newSettings.size) && !hasSeen) { + hasSeen = true; + const modalKey = openModal(modalProps => { closeModal(modalKey); }}> + + ); + } +} diff --git a/src/equicordplugins/newPluginsManager/index.tsx b/src/equicordplugins/newPluginsManager/index.tsx new file mode 100644 index 000000000..683d9de74 --- /dev/null +++ b/src/equicordplugins/newPluginsManager/index.tsx @@ -0,0 +1,28 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +import * as KnownSettings from "./knownSettings"; +import { KNOWN_PLUGINS_LEGACY_DATA_KEY, KNOWN_SETTINGS_DATA_KEY } from "./knownSettings"; +import { openNewPluginsModal } from "./NewPluginsModal"; + +export default definePlugin({ + name: "NewPluginsManager", + description: "Utility that notifies you when new plugins are added to Equicord", + authors: [Devs.Sqaaakoi], + enabledByDefault: true, + flux: { + async POST_CONNECTION_OPEN() { + openNewPluginsModal(); + } + }, + openNewPluginsModal, + KNOWN_PLUGINS_LEGACY_DATA_KEY, + KNOWN_SETTINGS_DATA_KEY, + KnownSettings +}); diff --git a/src/equicordplugins/newPluginsManager/knownSettings.ts b/src/equicordplugins/newPluginsManager/knownSettings.ts new file mode 100644 index 000000000..397d1390a --- /dev/null +++ b/src/equicordplugins/newPluginsManager/knownSettings.ts @@ -0,0 +1,91 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { DataStore } from "@api/index"; + +import plugins from "~plugins"; + +export type KnownPluginSettingsMap = Map>; + +export const KNOWN_PLUGINS_LEGACY_DATA_KEY = "NewPluginsManager_KnownPlugins"; +export const KNOWN_SETTINGS_DATA_KEY = "NewPluginsManager_KnownSettings"; + +function getSettingsSetForPlugin(plugin: string): Set { + const settings = plugins[plugin]?.settings?.def || {}; + return new Set(Object.keys(settings).filter(setting => setting !== "enabled")); +} + +function getCurrentSettings(pluginList: string[]): KnownPluginSettingsMap { + return new Map(pluginList.map(name => [ + name, + getSettingsSetForPlugin(name) + ])); +} + +export async function getKnownSettings(): Promise { + let map = await DataStore.get(KNOWN_SETTINGS_DATA_KEY) as KnownPluginSettingsMap; + if (map === undefined) { + const knownPlugins = await DataStore.get(KNOWN_PLUGINS_LEGACY_DATA_KEY) ?? [] as string[]; + const Plugins = [...Object.keys(plugins), ...knownPlugins]; + map = getCurrentSettings(Plugins); + DataStore.set(KNOWN_SETTINGS_DATA_KEY, map); + } + return map; +} + +export async function getNewSettings(): Promise { + const map = getCurrentSettings(Object.keys(plugins)); + const knownSettings = await getKnownSettings(); + map.forEach((settings, plugin) => { + const filteredSettings = [...settings].filter(setting => !knownSettings.get(plugin)?.has(setting)); + if (!filteredSettings.length) return map.delete(plugin); + map.set(plugin, new Set(filteredSettings)); + }); + return map; +} + +export async function getKnownPlugins(): Promise> { + const knownSettings = await getKnownSettings(); + return new Set(knownSettings.keys()); +} + +export async function getNewPlugins(): Promise> { + const currentPlugins = Object.keys(plugins); + const knownPlugins = await getKnownPlugins(); + return new Set(currentPlugins.filter(p => !knownPlugins.has(p))); +} + +export async function writeKnownSettings() { + const currentSettings = getCurrentSettings(Object.keys(plugins)); + const knownSettings = await getKnownSettings(); + const allSettings = new Map(); + new Set([...currentSettings.keys(), ...knownSettings.keys()]).forEach(plugin => { + allSettings.set(plugin, new Set([ + ...(currentSettings.get(plugin) || []), + ...(knownSettings.get(plugin) || []) + ])); + }); + await DataStore.set(KNOWN_SETTINGS_DATA_KEY, allSettings); +} + +export async function debugWipeSomeData() { + const settings = await getKnownSettings(); + settings.forEach((value, key) => { + if (Math.random() > 0.8) { + if (Math.random() > 0.5) return settings.set(key, new Set([...value].filter(() => Math.random() > 0.5))); + return settings.delete(key); + } + }); + await DataStore.set(KNOWN_SETTINGS_DATA_KEY, settings); +} + +export async function editRawData(patcher: (data: KnownPluginSettingsMap) => (Promise | any)) { + if (!patcher) return; + const map = await DataStore.get(KNOWN_SETTINGS_DATA_KEY) as KnownPluginSettingsMap; + const newMap = new Map(map); + await patcher(newMap); + await DataStore.set(KNOWN_SETTINGS_DATA_KEY, newMap ?? map); +} diff --git a/src/equicordplugins/newPluginsManager/styles.css b/src/equicordplugins/newPluginsManager/styles.css new file mode 100644 index 000000000..163505ae7 --- /dev/null +++ b/src/equicordplugins/newPluginsManager/styles.css @@ -0,0 +1,20 @@ +.vc-newPluginsManager-restart-tooltip { + max-width: 280px; +} + +.vc-newPluginsManager-restart-list { + padding-left: 1.25em; +} + +.vc-newPluginsManager-restart-list li { + list-style: disc; +} + +.vc-newPluginsManager-close > div { + display: flex; +} + +.vc-newPluginsManager-disable-wrapper { + display: flex; + margin: auto 0; +} \ No newline at end of file diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 081337afc..98f6ffc6c 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -166,42 +166,80 @@ const patchChannelContextMenu: NavContextMenuPatchCallback = ( ); }; -function renderDiffParts(diffParts: DiffPart[], message: Message) { - return diffParts.map((part, index) => { - const parsedContent = Parser.parse(part.text, true, { - channelId: message.channel_id, - messageId: message.id, - allowLinks: true, - allowHeading: true, - allowList: true, - allowEmojiLinks: true, - viewingChannelId: SelectedChannelStore.getChannelId(), - }); - - if (part.type === "unchanged") { - return React.createElement("span", { key: index }, parsedContent); - } else if (part.type === "added") { - return React.createElement("span", { - key: index, - className: "messagelogger-diff-added" - }, parsedContent); - } else if (part.type === "removed") { - return React.createElement("span", { - key: index, - className: "messagelogger-diff-removed" - }, parsedContent); - } - return React.createElement("span", { key: index }, parsedContent); +function createDiffSegment(part: DiffPart, message: Message, key: React.Key, highlightType?: "removed" | "added") { + const parsedContent = Parser.parse(part.text, true, { + channelId: message.channel_id, + messageId: message.id, + allowLinks: true, + allowHeading: true, + allowList: true, + allowEmojiLinks: true, + viewingChannelId: SelectedChannelStore.getChannelId(), }); + + let className: string | undefined; + if (part.type === "added" || part.type === "removed") { + if (!highlightType || highlightType === part.type) { + className = `messagelogger-diff-${part.type}`; + } + } + + return React.createElement("span", { key, className }, parsedContent); +} + +function renderDiffParts(diffParts: DiffPart[], message: Message) { + return diffParts.map((part, index) => createDiffSegment(part, message, index)); +} + +function renderFilteredDiffParts(diffParts: DiffPart[], message: Message, mode: "removed" | "added") { + const segments: React.ReactNode[] = []; + + diffParts.forEach((part, index) => { + if (mode === "removed" && part.type === "added") return; + if (mode === "added" && part.type === "removed") return; + + segments.push(createDiffSegment(part, message, `${mode}-${index}`, mode)); + }); + + return segments; } export function parseEditContent(content: string, message: Message, previousContent?: string) { const perMessageDiffEnabled = !disabledDiffMessages.has(message.id); if (previousContent && content !== previousContent && settings.store.showEditDiffs && perMessageDiffEnabled) { const diffParts = createMessageDiff(content, previousContent); + const useSeparatedDiffs = settings.store.separatedDiffs; + + if (useSeparatedDiffs) { + const highlightCurrent = previousContent === message.content; + const originalView = renderFilteredDiffParts(diffParts, message, "removed"); + + if (highlightCurrent) { + (message as any).customRenderedContent = { + __messageloggerDiff: true, + content: renderFilteredDiffParts(diffParts, message, "added"), + }; + } else if ((message as any).customRenderedContent?.__messageloggerDiff) { + delete (message as any).customRenderedContent; + } + + return React.createElement("div", { className: "messagelogger-diff-view" }, + React.createElement("div", { className: "messagelogger-diff-original" }, originalView), + !highlightCurrent && React.createElement("div", { className: "messagelogger-diff-updated" }, renderFilteredDiffParts(diffParts, message, "added")), + ); + } + + if ((message as any).customRenderedContent?.__messageloggerDiff) { + delete (message as any).customRenderedContent; + } + return renderDiffParts(diffParts, message); } + if ((message as any).customRenderedContent?.__messageloggerDiff) { + delete (message as any).customRenderedContent; + } + return Parser.parse(content, true, { channelId: message.channel_id, messageId: message.id, @@ -274,6 +312,21 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, description: "Show visual differences between edited message versions", default: false, + onChange: value => { + if (value && settings.store.separatedDiffs) { + settings.store.separatedDiffs = false; + } + }, + }, + separatedDiffs: { + type: OptionType.BOOLEAN, + description: "Separate addition and removals in diffs for a more readable differential", + default: false, + onChange: value => { + if (value && settings.store.showEditDiffs) { + settings.store.showEditDiffs = false; + } + }, }, }); diff --git a/src/plugins/messageLogger/messageLogger.css b/src/plugins/messageLogger/messageLogger.css index 11fb684f6..8ed5a3a21 100644 --- a/src/plugins/messageLogger/messageLogger.css +++ b/src/plugins/messageLogger/messageLogger.css @@ -84,6 +84,18 @@ pointer-events: none; } +.messagelogger-diff-view { + display: inline-flex; + flex-direction: column; + gap: 4px; +} + +.messagelogger-diff-original, +.messagelogger-diff-updated { + display: block; + white-space: pre-wrap; +} + /* when diffs are disabled for a specific message, remove diff colors */ .messagelogger-diff-disabled .messagelogger-diff-added, .messagelogger-diff-disabled .messagelogger-diff-removed {