feat(MessageLogger): add diffs to messagelogger (#356)
* fix(MessageLogger): add diffs to messagelogger >> fix multiple line edits not working and updating to the latest diff >> using character LCS algorithm >> Fix HistoryModal.tsx diff display logic >> update diff colors and deleteStyleOverlay.css * remove light colors, literally no need lol * add settings option * make the setting restart required * fix some duplication errors in logging with diffs * comment fixes * add the ability to disable/enable diff styles on a message * Update Setting --------- Co-authored-by: justjxke <Jake0300@protonmai.com> Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
f38d2643c8
commit
865097b0dc
5 changed files with 224 additions and 29 deletions
|
|
@ -84,7 +84,7 @@ export function HistoryModal({ modalProps, message }: { modalProps: ModalProps;
|
|||
</TabBar>
|
||||
|
||||
<div className={classes(CodeContainerClasses.markup, MiscClasses.messageContent, Margins.top20)}>
|
||||
{parseEditContent(contents[currentTab], message)}
|
||||
{parseEditContent(contents[currentTab], message, currentTab === contents.length - 1 ? undefined : contents[contents.length - 1])}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
115
src/plugins/messageLogger/diffUtils.ts
Normal file
115
src/plugins/messageLogger/diffUtils.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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<string>();
|
||||
|
||||
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(
|
||||
<Menu.MenuItem
|
||||
id={TOGGLE_DIFF_VIEW_ID}
|
||||
key={TOGGLE_DIFF_VIEW_ID}
|
||||
label={isDisabled ? "Enable Diff View" : "Disable Diff View"}
|
||||
color="danger"
|
||||
action={() => {
|
||||
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) => (
|
||||
<div key={idx} className="messagelogger-edited">
|
||||
{parseEditContent(edit.content, message)}
|
||||
<Timestamp
|
||||
timestamp={edit.timestamp}
|
||||
isEdited={true}
|
||||
isInline={false}
|
||||
>
|
||||
<span className={styles.edited}>{" "}({getIntlMessage("MESSAGE_EDITED")})</span>
|
||||
</Timestamp>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<React.Fragment key={disabledDiffMessages.has(messageId) ? `diff-off-${messageId}` : `diff-on-${messageId}`}>
|
||||
{message.editHistory?.map((edit, idx) => {
|
||||
const nextContent = idx === (message.editHistory?.length ?? 0) - 1
|
||||
? message.content
|
||||
: message.editHistory?.[idx + 1]?.content;
|
||||
|
||||
return (
|
||||
<div key={idx} className="messagelogger-edited">
|
||||
{parseEditContent(edit.content, message, nextContent)}
|
||||
<Timestamp
|
||||
timestamp={edit.timestamp}
|
||||
isEdited={true}
|
||||
isInline={false}
|
||||
>
|
||||
<span className={styles.edited}>{" "}({getIntlMessage("MESSAGE_EDITED")})</span>
|
||||
</Timestamp>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
}, { 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue