diff --git a/src/plugins/messageLogger/HistoryModal.tsx b/src/plugins/messageLogger/HistoryModal.tsx
index 5c49d72a7..c0b4d3dea 100644
--- a/src/plugins/messageLogger/HistoryModal.tsx
+++ b/src/plugins/messageLogger/HistoryModal.tsx
@@ -84,7 +84,7 @@ export function HistoryModal({ modalProps, message }: { modalProps: ModalProps;
- {parseEditContent(contents[currentTab], message)}
+ {parseEditContent(contents[currentTab], message, currentTab === contents.length - 1 ? undefined : contents[contents.length - 1])}
diff --git a/src/plugins/messageLogger/deleteStyleOverlay.css b/src/plugins/messageLogger/deleteStyleOverlay.css
index 59e4ac666..9fca958b2 100644
--- a/src/plugins/messageLogger/deleteStyleOverlay.css
+++ b/src/plugins/messageLogger/deleteStyleOverlay.css
@@ -1,3 +1,3 @@
.messagelogger-deleted {
- background-color: hsla(var(--red-430-hsl, 0 85% 61%) / 15%) !important;
-}
+ background-color: hsl(3deg 86% 68%) !important;
+}
\ No newline at end of file
diff --git a/src/plugins/messageLogger/diffUtils.ts b/src/plugins/messageLogger/diffUtils.ts
new file mode 100644
index 000000000..3713058a3
--- /dev/null
+++ b/src/plugins/messageLogger/diffUtils.ts
@@ -0,0 +1,115 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+export interface DiffPart {
+ type: "added" | "removed" | "unchanged";
+ text: string;
+}
+
+export function createWordDiff(oldText: string, newText: string): DiffPart[] {
+ // suffix shit, if oldText is shorter than newText and newText starts with oldText
+ if (oldText.length < newText.length && newText.startsWith(oldText)) {
+ const added = newText.slice(oldText.length);
+ const parts: DiffPart[] = [];
+ if (oldText.length > 0) {
+ parts.push({ type: "unchanged", text: oldText });
+ }
+ parts.push({ type: "added", text: added });
+ return parts;
+ }
+
+ // same as above
+ if (oldText.length < newText.length && newText.endsWith(oldText)) {
+ const added = newText.slice(0, newText.length - oldText.length);
+ const parts: DiffPart[] = [];
+ parts.push({ type: "added", text: added });
+ if (oldText.length > 0) {
+ parts.push({ type: "unchanged", text: oldText });
+ }
+ return parts;
+ }
+
+ // Handle simple prefix removal: "watch this" -> "watch"
+ if (newText.length < oldText.length && oldText.startsWith(newText)) {
+ const removed = oldText.slice(newText.length);
+ const parts: DiffPart[] = [];
+ if (newText.length > 0) {
+ parts.push({ type: "unchanged", text: newText });
+ }
+ parts.push({ type: "removed", text: removed });
+ return parts;
+ }
+
+ // Handle simple suffix removal: "prefix watch" -> "watch"
+ if (newText.length < oldText.length && oldText.endsWith(newText)) {
+ const removed = oldText.slice(0, oldText.length - newText.length);
+ const parts: DiffPart[] = [];
+ parts.push({ type: "removed", text: removed });
+ if (newText.length > 0) {
+ parts.push({ type: "unchanged", text: newText });
+ }
+ return parts;
+ }
+
+ // For complex cases, fall back to LCS algorithm
+ const oldChars = oldText.split("");
+ const newChars = newText.split("");
+
+ const result: DiffPart[] = [];
+ const dp: number[][] = [];
+
+ for (let i = 0; i <= oldChars.length; i++) {
+ dp[i] = [];
+ for (let j = 0; j <= newChars.length; j++) {
+ dp[i][j] = 0;
+ }
+ }
+
+ for (let i = 1; i <= oldChars.length; i++) {
+ for (let j = 1; j <= newChars.length; j++) {
+ if (oldChars[i - 1] === newChars[j - 1]) {
+ dp[i][j] = dp[i - 1][j - 1] + 1;
+ } else {
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
+ }
+ }
+ }
+
+ let i = oldChars.length;
+ let j = newChars.length;
+
+ const diffParts: DiffPart[] = [];
+
+ while (i > 0 || j > 0) {
+ if (i > 0 && j > 0 && oldChars[i - 1] === newChars[j - 1]) {
+ diffParts.unshift({ type: "unchanged", text: oldChars[i - 1] });
+ i--;
+ j--;
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] > dp[i - 1][j])) {
+ diffParts.unshift({ type: "added", text: newChars[j - 1] });
+ j--;
+ } else if (i > 0) {
+ diffParts.unshift({ type: "removed", text: oldChars[i - 1] });
+ i--;
+ }
+ }
+
+ const groupedParts: DiffPart[] = [];
+ for (const part of diffParts) {
+ const lastPart = groupedParts[groupedParts.length - 1];
+ if (lastPart && lastPart.type === part.type) {
+ lastPart.text += part.text;
+ } else {
+ groupedParts.push(part);
+ }
+ }
+
+ return groupedParts;
+}
+
+export function createMessageDiff(previousContent: string, currentContent: string): DiffPart[] {
+ return createWordDiff(previousContent, currentContent);
+}
diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx
index 7f2de1e67..57ab3f10b 100644
--- a/src/plugins/messageLogger/index.tsx
+++ b/src/plugins/messageLogger/index.tsx
@@ -21,20 +21,25 @@ import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack";
-import { ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common";
+import { ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, React, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common";
import overlayStyle from "./deleteStyleOverlay.css?managed";
import textStyle from "./deleteStyleText.css?managed";
+import { createMessageDiff, DiffPart } from "./diffUtils";
import { openHistoryModal } from "./HistoryModal";
interface MLMessage extends Message {
deleted?: boolean;
editHistory?: { timestamp: Date; content: string; }[];
firstEditTimestamp?: Date;
+ diffViewDisabled?: boolean;
}
const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessage");
+// track messages where the user disabled diffs for this session
+const disabledDiffMessages = new Set();
+
function addDeleteStyle() {
if (Settings.plugins.MessageLogger.deleteStyle === "text") {
enableStyle(textStyle);
@@ -47,6 +52,7 @@ function addDeleteStyle() {
const REMOVE_HISTORY_ID = "ml-remove-history";
const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style";
+const TOGGLE_DIFF_VIEW_ID = "ml-toggle-diff";
const patchMessageContextMenu: NavContextMenuPatchCallback = (
children,
props,
@@ -74,6 +80,28 @@ const patchMessageContextMenu: NavContextMenuPatchCallback = (
);
}
+ // toggle per-message diff rendering when the message has an edit history
+ if (editHistory?.length) {
+ const isDisabled = disabledDiffMessages.has(id);
+ children.push(
+ {
+ if (isDisabled) disabledDiffMessages.delete(id);
+ else disabledDiffMessages.add(id);
+ // also toggle a CSS class on the message element for an immediate visual effect (basically shows things work lol)
+ const domElement = document.getElementById(`chat-messages-${channel_id}-${id}`);
+ domElement?.classList.toggle("messagelogger-diff-disabled", disabledDiffMessages.has(id));
+ // force a re-render without mutating message fields
+ updateMessage(channel_id, id);
+ }}
+ />,
+ );
+ }
+
let label;
if (!Vencord.Plugins.isPluginEnabled("MessageLoggerEnhanced")) {
@@ -137,7 +165,32 @@ const patchChannelContextMenu: NavContextMenuPatchCallback = (
);
};
-export function parseEditContent(content: string, message: Message) {
+function renderDiffParts(diffParts: DiffPart[]) {
+ return diffParts.map((part, index) => {
+ if (part.type === "unchanged") {
+ return React.createElement("span", { key: index }, part.text);
+ } else if (part.type === "added") {
+ return React.createElement("span", {
+ key: index,
+ className: "messagelogger-diff-added"
+ }, part.text);
+ } else if (part.type === "removed") {
+ return React.createElement("span", {
+ key: index,
+ className: "messagelogger-diff-removed"
+ }, part.text);
+ }
+ return React.createElement("span", { key: index }, part.text);
+ });
+}
+
+export function parseEditContent(content: string, message: Message, previousContent?: string) {
+ const perMessageDiffEnabled = !disabledDiffMessages.has(message.id);
+ if (previousContent && content !== previousContent && Settings.plugins.MessageLogger.showEditDiffs && perMessageDiffEnabled) {
+ const diffParts = createMessageDiff(content, previousContent);
+ return renderDiffParts(diffParts);
+ }
+
return Parser.parse(content, true, {
channelId: message.channel_id,
messageId: message.id,
@@ -177,24 +230,33 @@ export default definePlugin({
[MessageStore],
() => MessageStore.getMessage(channelId, messageId) as MLMessage,
null,
- (oldMsg, newMsg) => oldMsg?.editHistory === newMsg?.editHistory,
+ (oldMsg, newMsg) =>
+ oldMsg?.editHistory === newMsg?.editHistory &&
+ oldMsg?.diffViewDisabled === newMsg?.diffViewDisabled &&
+ oldMsg === newMsg,
);
return Settings.plugins.MessageLogger.inlineEdits && (
- <>
- {message.editHistory?.map((edit, idx) => (
-
- {parseEditContent(edit.content, message)}
-
- {" "}({getIntlMessage("MESSAGE_EDITED")})
-
-
- ))}
- >
+
+ {message.editHistory?.map((edit, idx) => {
+ const nextContent = idx === (message.editHistory?.length ?? 0) - 1
+ ? message.content
+ : message.editHistory?.[idx + 1]?.content;
+
+ return (
+
+ {parseEditContent(edit.content, message, nextContent)}
+
+ {" "}({getIntlMessage("MESSAGE_EDITED")})
+
+
+ );
+ })}
+
);
}, { noop: true }),
@@ -262,6 +324,11 @@ export default definePlugin({
description: "Comma-separated list of guild IDs to ignore",
default: "",
},
+ showEditDiffs: {
+ type: OptionType.BOOLEAN,
+ description: "Show visual differences between edited message versions",
+ default: false,
+ },
},
handleDelete(
@@ -327,7 +394,7 @@ export default definePlugin({
) ||
(isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||
- // Ignore Venbot in the support channels
+ // Ignore Venbot in the support channels (love you venbot!!!)
(message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === VC_SUPPORT_CATEGORY_ID));
},
@@ -427,7 +494,8 @@ export default definePlugin({
"this.customRenderedContent = $1.customRenderedContent," +
"this.deleted = $1.deleted || false," +
"this.editHistory = $1.editHistory || []," +
- "this.firstEditTimestamp = $1.firstEditTimestamp || this.editedTimestamp || this.timestamp,",
+ "this.firstEditTimestamp = $1.firstEditTimestamp || this.editedTimestamp || this.timestamp," +
+ "this.diffViewDisabled = $1.diffViewDisabled || false,",
},
],
},
@@ -441,7 +509,7 @@ export default definePlugin({
match:
/(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
replace:
- "Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, firstEditTimestamp:$1.firstEditTimestamp })",
+ "Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, firstEditTimestamp:$1.firstEditTimestamp, diffViewDisabled:$1.diffViewDisabled })",
},
{
@@ -461,7 +529,8 @@ export default definePlugin({
"})())," +
"deleted: arguments[1]?.deleted," +
"editHistory: arguments[1]?.editHistory," +
- "firstEditTimestamp: new Date(arguments[1]?.firstEditTimestamp ?? $2.editedTimestamp ?? $2.timestamp)",
+ "firstEditTimestamp: new Date(arguments[1]?.firstEditTimestamp ?? $2.editedTimestamp ?? $2.timestamp)," +
+ "diffViewDisabled: arguments[1]?.diffViewDisabled",
},
{
// Preserve deleted attribute on attachments
diff --git a/src/plugins/messageLogger/messageLogger.css b/src/plugins/messageLogger/messageLogger.css
index a76e98888..5f4e5bb13 100644
--- a/src/plugins/messageLogger/messageLogger.css
+++ b/src/plugins/messageLogger/messageLogger.css
@@ -2,15 +2,12 @@
display: none;
}
-.messagelogger-deleted
-:is(
- .messagelogger-deleted-attachment,
+.messagelogger-deleted:is(.messagelogger-deleted-attachment,
.emoji,
[data-type="sticker"],
[class*="embedIframe"],
[class*="embedSpotify"],
- [class*="imageContainer"]
-) {
+ [class*="imageContainer"]) {
filter: grayscale(1) !important;
transition: 150ms filter ease-in-out;
@@ -48,3 +45,17 @@
flex-wrap: wrap;
gap: 16px;
}
+
+.messagelogger-diff-added {
+ color: #85E89D;
+}
+
+.messagelogger-diff-removed {
+ color: #F47068;
+}
+
+/* when diffs are disabled for a specific message, remove diff colors */
+.messagelogger-diff-disabled .messagelogger-diff-added,
+.messagelogger-diff-disabled .messagelogger-diff-removed {
+ color: inherit;
+}