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 <Jake0300@protonmai.com>
This commit is contained in:
Jake 2025-09-27 21:32:18 +01:00 • committed by GitHub
parent 835ec92c59
commit d695e3cac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 435 additions and 26 deletions

View file

@ -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<string>; newSettings: KnownPluginSettingsMap; }) {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
let updateContinueButton = () => { };
const depMap = React.useMemo(() => {
const o = {} as Record<string, string[]>;
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(
<Tooltip text={tooltipText} key={p.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => {
changes.handleChange(name);
updateContinueButton();
}}
disabled={true}
plugin={p}
key={p.name}
isNew={newPlugins.has(p.name)}
/>
)}
</Tooltip>
);
} else {
plugins.push(
<PluginCard
onRestartNeeded={name => {
changes.handleChange(name);
updateContinueButton();
}}
disabled={false}
plugin={p}
key={p.name}
isNew={newPlugins.has(p.name)}
/>
);
}
}
return <ModalRoot {...modalProps} size={ModalSize.MEDIUM} >
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>New Plugins and Settings ({[...plugins, ...requiredPlugins].length})</Text>
<Tooltip text="Dismiss for this session">
{tooltipProps =>
<div {...tooltipProps}>
<ModalCloseButton
onClick={modalProps.onClose}
className={classes(cl("close"), "vc-newPluginsManager-close")}
/>
</div>
}
</Tooltip>
</ModalHeader>
<ModalContent>
<div className={cl("grid")}>
{[...plugins, ...requiredPlugins]}
</div>
</ModalContent>
<ModalFooter>
<Flex direction={Flex.Direction.HORIZONTAL_REVERSE}>
<ContinueButton
close={modalProps.onClose}
changes={changes}
callback={(v: () => void) => updateContinueButton = v}
/>
</Flex>
<Flex direction={Flex.Direction.HORIZONTAL}>
<div className="vc-newPluginsManager-disable-wrapper">
<Checkbox
type="inverted"
value={!settings?.plugins?.NewPluginsManager?.enabled}
onChange={() => {
Settings.plugins.NewPluginsManager.enabled = !settings?.plugins?.NewPluginsManager?.enabled;
}}
>
<Text>Don't show this again</Text>
</Checkbox>
</div>
</Flex>
</ModalFooter>
</ModalRoot>;
}
function ContinueButton(props: { callback: (update: () => void) => void; changes: ChangeList<string>; close: () => any; }) {
const update = useForceUpdater();
props.callback(update);
return <Tooltip
tooltipClassName="vc-newPluginsManager-restart-tooltip"
text={<>
The following plugins require a restart:
<div className={Margins.bottom8} />
<ul className="vc-newPluginsManager-restart-list">
{props.changes.map(p => <li key={p}>{p}</li>)}
</ul>
</>}
shouldShow={props.changes.hasChanges}
>
{tooltipProps =>
<Button
{...tooltipProps}
color={Button.Colors.GREEN}
onClick={async () => {
await writeKnownSettings();
props.changes.hasChanges ? location.reload() : props.close();
}}
>
{props.changes.hasChanges ? "Restart" : "Continue"}
</Button>
}
</Tooltip>;
}
function makeDependencyList(deps: string[]) {
return (
<React.Fragment>
<Forms.FormText>This plugin is required by:</Forms.FormText>
{deps.map((dep: string) => <Forms.FormText key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)}
</React.Fragment>
);
}
export async function openNewPluginsModal() {
const newPlugins = await getNewPlugins();
const newSettings = await getNewSettings();
if ((newPlugins.size || newSettings.size) && !hasSeen) {
hasSeen = true;
const modalKey = openModal(modalProps => <ErrorBoundary noop onError={() => { closeModal(modalKey); }}>
<NewPluginsModal
modalProps={modalProps}
newPlugins={newPlugins}
newSettings={newSettings}
/>
</ErrorBoundary>);
}
}

View file

@ -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
});

View file

@ -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<string, Set<string>>;
export const KNOWN_PLUGINS_LEGACY_DATA_KEY = "NewPluginsManager_KnownPlugins";
export const KNOWN_SETTINGS_DATA_KEY = "NewPluginsManager_KnownSettings";
function getSettingsSetForPlugin(plugin: string): Set<string> {
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<KnownPluginSettingsMap> {
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<KnownPluginSettingsMap> {
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<Set<string>> {
const knownSettings = await getKnownSettings();
return new Set(knownSettings.keys());
}
export async function getNewPlugins(): Promise<Set<string>> {
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> | 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);
}

View file

@ -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;
}

View file

@ -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;
}
},
},
});

View file

@ -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 {