feat: port Antied & Antied Zero, add repository-wide blacklist enforcement
This commit is contained in:
parent
82eec74856
commit
d4f1332f5a
45 changed files with 1761 additions and 95 deletions
4
blacklist.json
Normal file
4
blacklist.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"updated_at": "2026-09-06T15:25:00.000Z",
|
||||||
|
"list": []
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { readFile, writeFile, readdir } from "fs/promises";
|
import { readFile, writeFile, readdir, copyFile } from "fs/promises";
|
||||||
import { extname, resolve as resolvePath } from "path";
|
import { extname, resolve as resolvePath } from "path";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
|
|
||||||
|
|
@ -108,6 +108,8 @@ for (const plug of await readdir("./plugins")) {
|
||||||
manifest.hash = createHash("sha256").update(toHash).digest("hex");
|
manifest.hash = createHash("sha256").update(toHash).digest("hex");
|
||||||
manifest.main = "index.js";
|
manifest.main = "index.js";
|
||||||
await writeFile(`./dist/${plug}/install/manifest.json`, JSON.stringify(manifest));
|
await writeFile(`./dist/${plug}/install/manifest.json`, JSON.stringify(manifest));
|
||||||
|
await writeFile(`./dist/${plug}/manifest.json`, JSON.stringify(manifest));
|
||||||
|
await copyFile(outPath, `./dist/${plug}/index.js`);
|
||||||
|
|
||||||
console.log(`Successfully built ${manifest.name}!`);
|
console.log(`Successfully built ${manifest.name}!`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -115,3 +117,7 @@ for (const plug of await readdir("./plugins")) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await copyFile("./blacklist.json", "./dist/blacklist.json");
|
||||||
|
} catch (_) {}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"name": "revenge-plugins",
|
"name": "revenge-plugins",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.mjs && node scripts/generate-site.mjs"
|
"build": "node build.mjs && node scripts/generate-site.mjs",
|
||||||
|
"serve": "node serve.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/helpers": "^0.5.17"
|
"@swc/helpers": "^0.5.17"
|
||||||
|
|
|
||||||
12
plugins/antied-zero/manifest.json
Normal file
12
plugins/antied-zero/manifest.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "Antied Zero",
|
||||||
|
"description": "Lightweight, zero-config message logger that keeps deleted messages and edit history visible.",
|
||||||
|
"authors": [
|
||||||
|
{ "name": "Angelw0lf" },
|
||||||
|
{ "name": "Rosie" }
|
||||||
|
],
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"vendetta": {
|
||||||
|
"icon": "ic_edit_24px"
|
||||||
|
}
|
||||||
|
}
|
||||||
45
plugins/antied-zero/src/index.ts
Normal file
45
plugins/antied-zero/src/index.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import { stopPlugin } from "@vendetta/plugins";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
|
|
||||||
|
import { patchFluxDispatch } from "./patches/fluxDispatch";
|
||||||
|
import { patchActionSheet } from "./patches/actionSheet";
|
||||||
|
import { patchSelfEdit } from "./patches/selfEdit";
|
||||||
|
import Settings from "./ui/Settings";
|
||||||
|
|
||||||
|
const isEnabledRef = { current: false };
|
||||||
|
const deletedMessageMap = new Map<string, any>();
|
||||||
|
const cleanups: (() => void)[] = [];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
onLoad: async () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
|
isEnabledRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
cleanups.push(patchFluxDispatch(deletedMessageMap, isEnabledRef));
|
||||||
|
cleanups.push(patchActionSheet(isEnabledRef));
|
||||||
|
cleanups.push(patchSelfEdit(isEnabledRef));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ANTIED ZERO] Crash on load:", err);
|
||||||
|
showToast("[ANTIED ZERO] Failed to load. Check debug log.");
|
||||||
|
stopPlugin(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload: () => {
|
||||||
|
isEnabledRef.current = false;
|
||||||
|
|
||||||
|
for (const fn of cleanups) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
cleanups.length = 0;
|
||||||
|
deletedMessageMap.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: Settings,
|
||||||
|
};
|
||||||
87
plugins/antied-zero/src/patches/actionSheet.tsx
Normal file
87
plugins/antied-zero/src/patches/actionSheet.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { before, after } from "@vendetta/patcher";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { findInReactTree } from "@vendetta/utils";
|
||||||
|
import { React, FluxDispatcher } from "@vendetta/metro/common";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
|
||||||
|
const ActionSheet = findByProps("openLazy", "hideActionSheet");
|
||||||
|
const MessageStore = findByProps("getMessage", "getMessages");
|
||||||
|
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
|
||||||
|
const ChannelMessages = findByProps("_channelMessages");
|
||||||
|
const { ActionSheetRow } = findByProps("ActionSheetRow") || {};
|
||||||
|
|
||||||
|
export const regexEscaper = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
|
||||||
|
export function patchActionSheet(isEnabledRef: { current: boolean }): () => void {
|
||||||
|
if (!ActionSheet?.openLazy || !ActionSheetRow) return () => {};
|
||||||
|
|
||||||
|
return before("openLazy", ActionSheet, ([component, args, actionMessage]: any[]) => {
|
||||||
|
if (!isEnabledRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = actionMessage?.message;
|
||||||
|
if (args !== "MessageLongPressActionSheet" || !message) return;
|
||||||
|
|
||||||
|
component.then((instance: any) => {
|
||||||
|
const unpatch = after("default", instance, (_: any, comp: any) => {
|
||||||
|
try {
|
||||||
|
React.useEffect(() => () => { unpatch(); }, []);
|
||||||
|
|
||||||
|
const isReplyBtn = (a: any) => a?.props?.label?.toLowerCase?.() === "reply";
|
||||||
|
const buttons = findInReactTree(comp, (c: any) => c?.find?.(isReplyBtn));
|
||||||
|
if (!buttons) return comp;
|
||||||
|
|
||||||
|
const position = Math.max(buttons.findIndex(isReplyBtn), buttons.length - 1);
|
||||||
|
|
||||||
|
let originalMessage = null;
|
||||||
|
if (message?.channel_id && message?.id) {
|
||||||
|
originalMessage = MessageStore?.getMessage?.(message.channel_id, message.id) ||
|
||||||
|
ChannelMessages?.get?.(message.channel_id)?.get?.(message.id);
|
||||||
|
}
|
||||||
|
if (!originalMessage) return comp;
|
||||||
|
|
||||||
|
const editedTag = "`[ EDITED ]`";
|
||||||
|
const separator = new RegExp(regexEscaper(editedTag), "gmi");
|
||||||
|
if (separator.test(message.content)) {
|
||||||
|
buttons.splice(
|
||||||
|
position || 1,
|
||||||
|
0,
|
||||||
|
React.createElement(ActionSheetRow, {
|
||||||
|
label: "Remove Edit History",
|
||||||
|
subLabel: "Antied Zero",
|
||||||
|
icon: React.createElement(ActionSheetRow.Icon, {
|
||||||
|
source: getAssetIDByName("ic_edit_24px") || getAssetIDByName("ic_message_edit"),
|
||||||
|
}),
|
||||||
|
onPress: () => {
|
||||||
|
const regexPattern = new RegExp(`(?:\\s${regexEscaper(editedTag)}\\n{2})`, "gm");
|
||||||
|
const lats = message?.content?.split(regexPattern);
|
||||||
|
const targetMessage = lats?.[lats.length - 1] ?? message.content;
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_UPDATE",
|
||||||
|
message: {
|
||||||
|
...message,
|
||||||
|
message_reference: message?.message_reference || message?.messageReference || null,
|
||||||
|
content: `${targetMessage}`,
|
||||||
|
guild_id: ChannelStore?.getChannel?.(originalMessage.channel_id)?.guild_id,
|
||||||
|
},
|
||||||
|
otherPluginBypass: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
ActionSheet.hideActionSheet();
|
||||||
|
showToast("Edit history removed", getAssetIDByName("ic_edit_24px"));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ANTIED ZERO] ActionSheet component error:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ANTIED ZERO] ActionSheet patch error:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
106
plugins/antied-zero/src/patches/fluxDispatch.ts
Normal file
106
plugins/antied-zero/src/patches/fluxDispatch.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { before } from "@vendetta/patcher";
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||||
|
import { recordToGateway } from "@shared/lib/recordToGateway";
|
||||||
|
|
||||||
|
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
|
||||||
|
const ChannelMessages = findByProps("_channelMessages");
|
||||||
|
const MessageStore = findByProps("getMessage", "getMessages");
|
||||||
|
|
||||||
|
export function patchFluxDispatch(deletedMessageMap: Map<string, any>, isEnabledRef: { current: boolean }): () => void {
|
||||||
|
return before("dispatch", FluxDispatcher, (args: any[]) => {
|
||||||
|
if (!isEnabledRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ev = args[0];
|
||||||
|
if (!ev || !ev.type) return;
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
MESSAGE_DELETE
|
||||||
|
==========================================================*/
|
||||||
|
if (ev.type === "MESSAGE_DELETE") {
|
||||||
|
if (ev.otherPluginBypass || ev.manualDelete) return;
|
||||||
|
|
||||||
|
const orig = ChannelMessages?.get?.(ev.channelId)?.get?.(ev.id) || MessageStore?.getMessage?.(ev.channelId, ev.id) || deletedMessageMap.get(ev.id)?.original;
|
||||||
|
if (!orig?.author?.id || !orig.author.username) return;
|
||||||
|
|
||||||
|
// Ephemeral dismiss from bots check
|
||||||
|
if (orig.author.bot && (orig.flags === 64 || (orig.flags & 64) === 64)) return;
|
||||||
|
if (orig.author.bot) return;
|
||||||
|
|
||||||
|
// Empty check
|
||||||
|
if (!orig.content && !orig.attachments?.length && !orig.embeds?.length) return;
|
||||||
|
|
||||||
|
if (deletedMessageMap.has(ev.id)) {
|
||||||
|
ev.type = "MESSAGE_UPDATE";
|
||||||
|
ev.channelId = orig.channel_id || ev.channelId;
|
||||||
|
ev.message = {
|
||||||
|
id: ev.id,
|
||||||
|
channel_id: orig.channel_id || ev.channelId,
|
||||||
|
flags: 64,
|
||||||
|
was_deleted: true,
|
||||||
|
};
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildId = ChannelStore?.getChannel?.(orig.channel_id || ev.channelId)?.guild_id;
|
||||||
|
const gatewayOrig = recordToGateway(orig);
|
||||||
|
|
||||||
|
ev.message = {
|
||||||
|
...gatewayOrig,
|
||||||
|
content: orig.content,
|
||||||
|
channel_id: orig.channel_id || ev.channelId,
|
||||||
|
guild_id: guildId,
|
||||||
|
flags: 64,
|
||||||
|
was_deleted: true,
|
||||||
|
message_reference: orig?.message_reference || orig?.messageReference || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
ev.type = "MESSAGE_UPDATE";
|
||||||
|
ev.channelId = orig.channel_id || ev.channelId;
|
||||||
|
ev.optimistic = false;
|
||||||
|
ev.sendMessageOptions = {};
|
||||||
|
ev.isPushNotification = false;
|
||||||
|
|
||||||
|
deletedMessageMap.set(ev.id, { message: args, original: orig });
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
MESSAGE_UPDATE
|
||||||
|
==========================================================*/
|
||||||
|
if (ev.type === "MESSAGE_UPDATE") {
|
||||||
|
if (ev.otherPluginBypass) return;
|
||||||
|
const msg = ev.message;
|
||||||
|
if (!msg || msg.author?.bot) return;
|
||||||
|
|
||||||
|
// Only handle real content edits with a valid edited_timestamp
|
||||||
|
if (!msg.edited_timestamp || msg.edited_timestamp === "invalid_timestamp") return;
|
||||||
|
|
||||||
|
const chId = msg.channel_id || ev.channelId;
|
||||||
|
const id = msg.id || ev.id;
|
||||||
|
if (!chId || !id) return;
|
||||||
|
|
||||||
|
const orig = MessageStore?.getMessage?.(chId, id) || ChannelMessages?.get?.(chId)?.get?.(id);
|
||||||
|
if (!orig?.author?.id || !orig.author.username) return;
|
||||||
|
if (!orig.content || !msg.content || msg.content === orig.content) return;
|
||||||
|
|
||||||
|
const prefix = "`[ EDITED ]`\n\n";
|
||||||
|
const gatewayOrig = recordToGateway(orig);
|
||||||
|
|
||||||
|
ev.message = {
|
||||||
|
...gatewayOrig,
|
||||||
|
...msg,
|
||||||
|
content: `${orig.content} ${prefix}${msg.content}`,
|
||||||
|
guild_id: ChannelStore?.getChannel?.(chId)?.guild_id ?? msg.guild_id,
|
||||||
|
edited_timestamp: "invalid_timestamp",
|
||||||
|
message_reference: msg?.message_reference || orig?.messageReference || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ANTIED ZERO] Flux error:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
23
plugins/antied-zero/src/patches/selfEdit.ts
Normal file
23
plugins/antied-zero/src/patches/selfEdit.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { before } from "@vendetta/patcher";
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { regexEscaper } from "./actionSheet";
|
||||||
|
|
||||||
|
const Message = findByProps("sendMessage", "startEditMessage");
|
||||||
|
|
||||||
|
export function patchSelfEdit(isEnabledRef: { current: boolean }): () => void {
|
||||||
|
if (!Message?.startEditMessage) return () => {};
|
||||||
|
|
||||||
|
return before("startEditMessage", Message, (args: any[]) => {
|
||||||
|
if (!isEnabledRef.current || !args?.length) return;
|
||||||
|
|
||||||
|
const edited = "`[ EDITED ]`";
|
||||||
|
const dan = regexEscaper(edited);
|
||||||
|
const regexPattern = new RegExp(`(?:\\s${dan}\\n{2})`, "gm");
|
||||||
|
|
||||||
|
const msg = args[2];
|
||||||
|
if (typeof msg === "string") {
|
||||||
|
const lats = msg.split(regexPattern);
|
||||||
|
args[2] = lats[lats.length - 1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
16
plugins/antied-zero/src/ui/Settings.tsx
Normal file
16
plugins/antied-zero/src/ui/Settings.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { React } from "@vendetta/metro/common";
|
||||||
|
import SettingsScaffold from "@shared/ui/SettingsScaffold";
|
||||||
|
import NoteBox from "@shared/ui/NoteBox";
|
||||||
|
import { TableRowGroup } from "@shared/ui/table";
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
return (
|
||||||
|
<SettingsScaffold>
|
||||||
|
<TableRowGroup title="Antied Zero">
|
||||||
|
<NoteBox>
|
||||||
|
Antied Zero is active and running in zero-config mode. Deleted messages are automatically retained with ephemeral flags, and edit history is tracked in-line.
|
||||||
|
</NoteBox>
|
||||||
|
</TableRowGroup>
|
||||||
|
</SettingsScaffold>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
plugins/antied/manifest.json
Normal file
12
plugins/antied/manifest.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "Cute Moodle (Antied)",
|
||||||
|
"description": "Keeps deleted messages and edit history visible in chat with customizable highlights, indicators, and actionsheet management.",
|
||||||
|
"authors": [
|
||||||
|
{ "name": "Angelw0lf" },
|
||||||
|
{ "name": "Rosie" }
|
||||||
|
],
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"vendetta": {
|
||||||
|
"icon": "ic_edit_24px"
|
||||||
|
}
|
||||||
|
}
|
||||||
148
plugins/antied/src/index.ts
Normal file
148
plugins/antied/src/index.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { storage, id } from "@vendetta/plugin";
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||||
|
import { stopPlugin } from "@vendetta/plugins";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import { makeDefaults } from "@shared/lib/utility";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
|
|
||||||
|
import { patchFluxDispatch } from "./patches/fluxDispatch";
|
||||||
|
import { patchUpdateRows } from "./patches/updateRows";
|
||||||
|
import { patchActionSheet } from "./patches/actionSheet";
|
||||||
|
import { patchSelfEdit } from "./patches/selfEdit";
|
||||||
|
import { patchContextMenu } from "./patches/contextMenu";
|
||||||
|
import { patchMessageRecord } from "./patches/messageRecord";
|
||||||
|
import Settings from "./ui/Settings";
|
||||||
|
import type { AntiedStorage } from "./lib/types";
|
||||||
|
|
||||||
|
const ChannelMessages = findByProps("_channelMessages");
|
||||||
|
|
||||||
|
const isEnabledRef = { current: false };
|
||||||
|
const deletedMessageMap = new Map<string, any>();
|
||||||
|
const cleanups: (() => void)[] = [];
|
||||||
|
let intervalPurge: any = null;
|
||||||
|
|
||||||
|
const KEEP_NEWEST = 10;
|
||||||
|
const DELETE_EACH_CYCLE = 140;
|
||||||
|
|
||||||
|
function initStorage() {
|
||||||
|
makeDefaults(storage, {
|
||||||
|
setting: {
|
||||||
|
colorpick: false,
|
||||||
|
customize: false,
|
||||||
|
ingorelist: false,
|
||||||
|
patches: false,
|
||||||
|
text: false,
|
||||||
|
timestamp: false,
|
||||||
|
},
|
||||||
|
switches: {
|
||||||
|
customizeable: false,
|
||||||
|
enableMD: true,
|
||||||
|
enableMU: true,
|
||||||
|
useBackgroundColor: false,
|
||||||
|
useSemRawColors: false,
|
||||||
|
ignoreBots: false,
|
||||||
|
ignoreOwnMessages: false,
|
||||||
|
minimalistic: true,
|
||||||
|
alwaysAdd: false,
|
||||||
|
darkMode: true,
|
||||||
|
removeDismissButton: false,
|
||||||
|
addTimestampForEdits: false,
|
||||||
|
timestampStyle: "R",
|
||||||
|
useEphemeralForDeleted: true,
|
||||||
|
overrideIndicator: false,
|
||||||
|
useIndicatorForDeleted: false,
|
||||||
|
useCustomPluginName: false,
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
textColor: "#E40303",
|
||||||
|
backgroundColor: "#FF2C2F",
|
||||||
|
backgroundColorAlpha: "33",
|
||||||
|
gutterColor: "#FF2C2F",
|
||||||
|
gutterColorAlpha: "CC",
|
||||||
|
semRawColorPrefix: "semanticColors.TEXT_BRAND",
|
||||||
|
},
|
||||||
|
inputs: {
|
||||||
|
deletedMessageBuffer: "This message is deleted",
|
||||||
|
editedMessageBuffer: "`[ EDITED ]`",
|
||||||
|
historyToast: "[ANTI ED] History Removed",
|
||||||
|
ignoredUserList: [],
|
||||||
|
customPluginName: "ANTIED",
|
||||||
|
customIndicator: "",
|
||||||
|
},
|
||||||
|
misc: {
|
||||||
|
timestampPos: "BEFORE",
|
||||||
|
editHistoryIcon: "ic_edit_24px",
|
||||||
|
},
|
||||||
|
debug: false,
|
||||||
|
debugUpdateRows: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
onLoad: async () => {
|
||||||
|
initStorage();
|
||||||
|
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
|
isEnabledRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
cleanups.push(patchMessageRecord(isEnabledRef));
|
||||||
|
cleanups.push(patchFluxDispatch(deletedMessageMap, isEnabledRef));
|
||||||
|
cleanups.push(patchUpdateRows(deletedMessageMap, isEnabledRef));
|
||||||
|
cleanups.push(patchActionSheet(deletedMessageMap, isEnabledRef));
|
||||||
|
cleanups.push(patchSelfEdit(isEnabledRef));
|
||||||
|
cleanups.push(patchContextMenu(isEnabledRef));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ANTIED] Crash on load:", err);
|
||||||
|
showToast("[ANTIED] Failed to load patches. Check debug log.");
|
||||||
|
stopPlugin(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalPurge = setInterval(() => {
|
||||||
|
if (deletedMessageMap.size <= KEEP_NEWEST) return;
|
||||||
|
const toDelete = Math.min(DELETE_EACH_CYCLE, deletedMessageMap.size - KEEP_NEWEST);
|
||||||
|
let i = 0;
|
||||||
|
for (const key of deletedMessageMap.keys()) {
|
||||||
|
deletedMessageMap.delete(key);
|
||||||
|
if (++i >= toDelete) break;
|
||||||
|
}
|
||||||
|
}, 15 * 60 * 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload: () => {
|
||||||
|
isEnabledRef.current = false;
|
||||||
|
if (intervalPurge) clearInterval(intervalPurge);
|
||||||
|
|
||||||
|
for (const fn of cleanups) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
cleanups.length = 0;
|
||||||
|
|
||||||
|
// Revert faked messages
|
||||||
|
if (ChannelMessages?._channelMessages) {
|
||||||
|
for (const channelId in ChannelMessages._channelMessages) {
|
||||||
|
const arr = ChannelMessages._channelMessages[channelId]?._array;
|
||||||
|
if (!Array.isArray(arr)) continue;
|
||||||
|
for (const message of arr) {
|
||||||
|
if (message?.was_deleted) {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_DELETE",
|
||||||
|
id: message.id,
|
||||||
|
channelId: message.channel_id || channelId,
|
||||||
|
otherPluginBypass: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedMessageMap.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: Settings,
|
||||||
|
};
|
||||||
51
plugins/antied/src/lib/types.ts
Normal file
51
plugins/antied/src/lib/types.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
export interface AntiedStorage {
|
||||||
|
setting: {
|
||||||
|
colorpick: boolean;
|
||||||
|
customize: boolean;
|
||||||
|
ingorelist: boolean;
|
||||||
|
patches: boolean;
|
||||||
|
text: boolean;
|
||||||
|
timestamp: boolean;
|
||||||
|
};
|
||||||
|
switches: {
|
||||||
|
customizeable: boolean;
|
||||||
|
enableMD: boolean;
|
||||||
|
enableMU: boolean;
|
||||||
|
useBackgroundColor: boolean;
|
||||||
|
useSemRawColors: boolean;
|
||||||
|
ignoreBots: boolean;
|
||||||
|
ignoreOwnMessages: boolean;
|
||||||
|
minimalistic: boolean;
|
||||||
|
alwaysAdd: boolean;
|
||||||
|
darkMode: boolean;
|
||||||
|
removeDismissButton: boolean;
|
||||||
|
addTimestampForEdits: boolean;
|
||||||
|
timestampStyle: "t" | "T" | "d" | "D" | "f" | "F" | "R";
|
||||||
|
useEphemeralForDeleted: boolean;
|
||||||
|
overrideIndicator: boolean;
|
||||||
|
useIndicatorForDeleted: boolean;
|
||||||
|
useCustomPluginName: boolean;
|
||||||
|
};
|
||||||
|
colors: {
|
||||||
|
textColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
backgroundColorAlpha: string;
|
||||||
|
gutterColor: string;
|
||||||
|
gutterColorAlpha: string;
|
||||||
|
semRawColorPrefix: string;
|
||||||
|
};
|
||||||
|
inputs: {
|
||||||
|
deletedMessageBuffer: string;
|
||||||
|
editedMessageBuffer: string;
|
||||||
|
historyToast: string;
|
||||||
|
ignoredUserList: { id: string; username: string }[];
|
||||||
|
customPluginName: string;
|
||||||
|
customIndicator: string;
|
||||||
|
};
|
||||||
|
misc: {
|
||||||
|
timestampPos: "BEFORE" | "AFTER";
|
||||||
|
editHistoryIcon: string;
|
||||||
|
};
|
||||||
|
debug: boolean;
|
||||||
|
debugUpdateRows: boolean;
|
||||||
|
}
|
||||||
126
plugins/antied/src/patches/actionSheet.tsx
Normal file
126
plugins/antied/src/patches/actionSheet.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { before, after } from "@vendetta/patcher";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { findInReactTree } from "@vendetta/utils";
|
||||||
|
import { React, FluxDispatcher } from "@vendetta/metro/common";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import { storage } from "@vendetta/plugin";
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import type { AntiedStorage } from "../lib/types";
|
||||||
|
|
||||||
|
const ActionSheet = findByProps("openLazy", "hideActionSheet");
|
||||||
|
const MessageStore = findByProps("getMessage", "getMessages");
|
||||||
|
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
|
||||||
|
const ChannelMessages = findByProps("_channelMessages");
|
||||||
|
const { ActionSheetRow } = findByProps("ActionSheetRow") || {};
|
||||||
|
|
||||||
|
export const regexEscaper = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
|
||||||
|
export function patchActionSheet(deletedMessageMap: Map<string, any>, isEnabledRef: { current: boolean }): () => void {
|
||||||
|
if (!ActionSheet?.openLazy || !ActionSheetRow) return () => {};
|
||||||
|
|
||||||
|
return before("openLazy", ActionSheet, ([component, args, actionMessage]: any[]) => {
|
||||||
|
if (!isEnabledRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = actionMessage?.message;
|
||||||
|
if (args !== "MessageLongPressActionSheet" || !message) return;
|
||||||
|
|
||||||
|
component.then((instance: any) => {
|
||||||
|
const unpatch = after("default", instance, (_: any, comp: any) => {
|
||||||
|
try {
|
||||||
|
React.useEffect(() => () => { unpatch(); }, []);
|
||||||
|
|
||||||
|
const isReplyBtn = (a: any) => a?.props?.label?.toLowerCase?.() === "reply";
|
||||||
|
const buttons = findInReactTree(comp, (c: any) => c?.find?.(isReplyBtn));
|
||||||
|
if (!buttons) return comp;
|
||||||
|
|
||||||
|
const position = Math.max(buttons.findIndex(isReplyBtn), buttons.length - 1);
|
||||||
|
|
||||||
|
let originalMessage = null;
|
||||||
|
if (message?.channel_id && message?.id) {
|
||||||
|
originalMessage = MessageStore?.getMessage?.(message.channel_id, message.id) ||
|
||||||
|
ChannelMessages?.get?.(message.channel_id)?.get?.(message.id);
|
||||||
|
}
|
||||||
|
if (!originalMessage) return comp;
|
||||||
|
|
||||||
|
const cfg = storage as AntiedStorage;
|
||||||
|
const escapedBuffer = regexEscaper(cfg.inputs?.editedMessageBuffer || "`[ EDITED ]`");
|
||||||
|
const separator = new RegExp(escapedBuffer, "gmi");
|
||||||
|
const hasBuffer = separator.test(message.content);
|
||||||
|
|
||||||
|
if (hasBuffer) {
|
||||||
|
const targetPos = position || 1;
|
||||||
|
buttons.splice(
|
||||||
|
targetPos,
|
||||||
|
0,
|
||||||
|
React.createElement(ActionSheetRow, {
|
||||||
|
label: "Remove Edit History",
|
||||||
|
subLabel: "Cute Moodle (Antied)",
|
||||||
|
icon: React.createElement(ActionSheetRow.Icon, {
|
||||||
|
source: getAssetIDByName("ic_edit_24px") || getAssetIDByName("ic_message_edit"),
|
||||||
|
}),
|
||||||
|
onPress: () => {
|
||||||
|
const regexPattern = new RegExp(
|
||||||
|
`(?:(?:\\s${escapedBuffer}(\\s\\(<t:\\d+:[tTdDfFR]>\\))?\\n{2})|(?:(?:\\s\\(<t:\\d+:[tTdDfFR]>\\) ${escapedBuffer}\\n{2})))`,
|
||||||
|
"gm"
|
||||||
|
);
|
||||||
|
const lats = message?.content?.split(regexPattern);
|
||||||
|
const targetMessage = lats?.[lats.length - 1] ?? message.content;
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_UPDATE",
|
||||||
|
message: {
|
||||||
|
...message,
|
||||||
|
message_reference: message?.message_reference || message?.messageReference || null,
|
||||||
|
content: `${targetMessage}`,
|
||||||
|
guild_id: ChannelStore?.getChannel?.(originalMessage.channel_id)?.guild_id,
|
||||||
|
},
|
||||||
|
otherPluginBypass: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
ActionSheet.hideActionSheet();
|
||||||
|
if (cfg.inputs?.historyToast) {
|
||||||
|
showToast(cfg.inputs.historyToast, getAssetIDByName(cfg.misc?.editHistoryIcon || "ic_edit_24px"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg.switches?.useEphemeralForDeleted && deletedMessageMap.has(message.id)) {
|
||||||
|
const targetPos = position || 1;
|
||||||
|
buttons.splice(
|
||||||
|
targetPos,
|
||||||
|
0,
|
||||||
|
React.createElement(ActionSheetRow, {
|
||||||
|
label: "Remove Deleted Message",
|
||||||
|
subLabel: "Cute Moodle (Antied)",
|
||||||
|
isDestructive: true,
|
||||||
|
icon: React.createElement(ActionSheetRow.Icon, {
|
||||||
|
source: getAssetIDByName("ic_edit_24px") || getAssetIDByName("TrashIcon"),
|
||||||
|
}),
|
||||||
|
onPress: () => {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_DELETE",
|
||||||
|
guildId: ChannelStore?.getChannel?.(originalMessage.channel_id)?.guild_id,
|
||||||
|
id: message?.id,
|
||||||
|
channelId: message?.channel_id,
|
||||||
|
otherPluginBypass: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
ActionSheet.hideActionSheet();
|
||||||
|
showToast("[ANTIED] Message Removed", getAssetIDByName("ic_edit_24px"));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ANTIED] ActionSheet component error:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ANTIED] ActionSheet patch error:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
71
plugins/antied/src/patches/contextMenu.ts
Normal file
71
plugins/antied/src/patches/contextMenu.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { after } from "@vendetta/patcher";
|
||||||
|
import { findByName, findByStoreName } from "@vendetta/metro";
|
||||||
|
import { storage } from "@vendetta/plugin";
|
||||||
|
import { findInReactTree } from "@vendetta/utils";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import type { AntiedStorage } from "../lib/types";
|
||||||
|
|
||||||
|
const PROFILE_MENU_NAMES = ["UserProfileOverflowMenu", "BotUserProfileOverflowMenu"];
|
||||||
|
const UserStore = findByStoreName("UserStore");
|
||||||
|
|
||||||
|
function getMainItems(ret: any): any[] | null {
|
||||||
|
let items = ret?.props?.items;
|
||||||
|
if (Array.isArray(items) && Array.isArray(items[0])) return items[0];
|
||||||
|
items = ret?.props?.children?.props?.items;
|
||||||
|
if (Array.isArray(items) && Array.isArray(items[0])) return items[0];
|
||||||
|
const node = findInReactTree(ret, (n: any) => Array.isArray(n?.props?.items) && Array.isArray(n.props.items[0]));
|
||||||
|
return node?.props?.items?.[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchContextMenu(isEnabledRef: { current: boolean }): () => void {
|
||||||
|
const unpatches: (() => void)[] = [];
|
||||||
|
|
||||||
|
for (const name of PROFILE_MENU_NAMES) {
|
||||||
|
const mod = findByName(name, false);
|
||||||
|
if (!mod) continue;
|
||||||
|
|
||||||
|
unpatches.push(
|
||||||
|
after("default", mod, (args: any[], ret: any) => {
|
||||||
|
if (!isEnabledRef.current) return;
|
||||||
|
try {
|
||||||
|
const props = args[0] ?? {};
|
||||||
|
const userId = props.user?.id || props.userId;
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
const items = getMainItems(ret);
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
if (items.some((item: any) => item?.label?.includes("(Antied)"))) return;
|
||||||
|
|
||||||
|
const cfg = storage as AntiedStorage;
|
||||||
|
cfg.inputs ??= {} as any;
|
||||||
|
cfg.inputs.ignoredUserList ??= [];
|
||||||
|
|
||||||
|
const isIgnored = cfg.inputs.ignoredUserList.some((u) => u.id === userId);
|
||||||
|
const user = UserStore?.getUser?.(userId);
|
||||||
|
const username = user?.username || userId;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: isIgnored ? "Unignore User (Antied)" : "Ignore User (Antied)",
|
||||||
|
isDestructive: !isIgnored,
|
||||||
|
action: () => {
|
||||||
|
if (isIgnored) {
|
||||||
|
cfg.inputs.ignoredUserList = cfg.inputs.ignoredUserList.filter((u) => u.id !== userId);
|
||||||
|
showToast(`Unignored ${username} in Antied`);
|
||||||
|
} else {
|
||||||
|
cfg.inputs.ignoredUserList.push({ id: userId, username });
|
||||||
|
showToast(`Ignored ${username} in Antied`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ANTIED] Context menu error:", e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const fn of unpatches) fn();
|
||||||
|
};
|
||||||
|
}
|
||||||
175
plugins/antied/src/patches/fluxDispatch.ts
Normal file
175
plugins/antied/src/patches/fluxDispatch.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
import { before } from "@vendetta/patcher";
|
||||||
|
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||||
|
import { storage } from "@vendetta/plugin";
|
||||||
|
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||||
|
import { recordToGateway } from "@shared/lib/recordToGateway";
|
||||||
|
import type { AntiedStorage } from "../lib/types";
|
||||||
|
|
||||||
|
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
|
||||||
|
const ChannelMessages = findByProps("_channelMessages");
|
||||||
|
const MessageStore = findByProps("getMessage", "getMessages");
|
||||||
|
const UserStore = findByStoreName("UserStore");
|
||||||
|
const AuthStore = findByStoreName("AuthenticationStore") || findByProps("getToken");
|
||||||
|
|
||||||
|
function getCurrentUserId(): string | undefined {
|
||||||
|
return UserStore?.getCurrentUser?.()?.id || AuthStore?.getId?.() || AuthStore?.getCurrentUser?.()?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = () => Date.now();
|
||||||
|
const tsStyle = () => {
|
||||||
|
const s = (storage as AntiedStorage).switches?.timestampStyle;
|
||||||
|
return s && "tTdDfFR".includes(s) ? s : "R";
|
||||||
|
};
|
||||||
|
|
||||||
|
export function patchFluxDispatch(deletedMessageMap: Map<string, any>, isEnabledRef: { current: boolean }): () => void {
|
||||||
|
return before("dispatch", FluxDispatcher, (args: any[]) => {
|
||||||
|
if (!isEnabledRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ev = args[0];
|
||||||
|
if (!ev || !ev.type) return;
|
||||||
|
|
||||||
|
const cfg = storage as AntiedStorage;
|
||||||
|
const currentUserId = getCurrentUserId();
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
MESSAGE_DELETE
|
||||||
|
==========================================================*/
|
||||||
|
if (ev.type === "MESSAGE_DELETE") {
|
||||||
|
if (!cfg.switches?.enableMD || ev.otherPluginBypass || ev.manualDelete) return;
|
||||||
|
|
||||||
|
const orig = ChannelMessages?.get?.(ev.channelId)?.get?.(ev.id) || MessageStore?.getMessage?.(ev.channelId, ev.id) || deletedMessageMap.get(ev.id)?.original;
|
||||||
|
if (!orig?.author?.id || !orig.author.username) return;
|
||||||
|
|
||||||
|
// Ephemeral message dismiss check
|
||||||
|
if (orig?.author?.bot && (orig?.flags === 64 || (orig?.flags & 64) === 64)) return;
|
||||||
|
|
||||||
|
// Empty message check
|
||||||
|
if (!orig.content && !orig.attachments?.length && !orig.embeds?.length && !orig.components?.length) return;
|
||||||
|
|
||||||
|
// Bot check
|
||||||
|
if (cfg.switches.ignoreBots && (orig.author.bot || orig.author.isNonUserBot?.())) return;
|
||||||
|
|
||||||
|
// Own message check
|
||||||
|
if (cfg.switches.ignoreOwnMessages && orig.author.id === currentUserId) return;
|
||||||
|
|
||||||
|
// Ignored users list check
|
||||||
|
if (cfg.inputs?.ignoredUserList?.length) {
|
||||||
|
if (cfg.inputs.ignoredUserList.some((u) => u.id === orig.author.id || u.username === orig.author.username)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedMessageMap.has(ev.id)) {
|
||||||
|
ev.type = "MESSAGE_UPDATE";
|
||||||
|
ev.channelId = orig.channel_id || ev.channelId;
|
||||||
|
ev.message = {
|
||||||
|
id: ev.id,
|
||||||
|
channel_id: orig.channel_id || ev.channelId,
|
||||||
|
was_deleted: true,
|
||||||
|
};
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildId = ChannelStore?.getChannel?.(orig.channel_id || ev.channelId)?.guild_id;
|
||||||
|
const gatewayOrig = recordToGateway(orig);
|
||||||
|
|
||||||
|
ev.message = {
|
||||||
|
...gatewayOrig,
|
||||||
|
content: orig.content,
|
||||||
|
channel_id: orig.channel_id || ev.channelId,
|
||||||
|
guild_id: guildId,
|
||||||
|
was_deleted: true,
|
||||||
|
message_reference: orig?.message_reference || orig?.messageReference || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
ev.type = "MESSAGE_UPDATE";
|
||||||
|
ev.channelId = orig.channel_id || ev.channelId;
|
||||||
|
ev.optimistic = false;
|
||||||
|
ev.sendMessageOptions = {};
|
||||||
|
ev.isPushNotification = false;
|
||||||
|
|
||||||
|
deletedMessageMap.set(ev.id, { message: args, original: orig });
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
MESSAGE_DELETE_BULK
|
||||||
|
==========================================================*/
|
||||||
|
if (ev.type === "MESSAGE_DELETE_BULK") {
|
||||||
|
if (!cfg.switches?.enableMD || ev.otherPluginBypass || !Array.isArray(ev.ids)) return;
|
||||||
|
|
||||||
|
for (const id of ev.ids) {
|
||||||
|
const orig = ChannelMessages?.get?.(ev.channelId)?.get?.(id) || MessageStore?.getMessage?.(ev.channelId, id);
|
||||||
|
if (!orig?.author?.id) continue;
|
||||||
|
if (cfg.switches.ignoreBots && (orig.author.bot || orig.author.isNonUserBot?.())) continue;
|
||||||
|
if (cfg.switches.ignoreOwnMessages && orig.author.id === currentUserId) continue;
|
||||||
|
|
||||||
|
deletedMessageMap.set(id, { original: orig });
|
||||||
|
|
||||||
|
const gatewayOrig = recordToGateway(orig);
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_UPDATE",
|
||||||
|
channelId: ev.channelId,
|
||||||
|
message: {
|
||||||
|
...gatewayOrig,
|
||||||
|
was_deleted: true,
|
||||||
|
},
|
||||||
|
otherPluginBypass: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
MESSAGE_UPDATE
|
||||||
|
==========================================================*/
|
||||||
|
if (ev.type === "MESSAGE_UPDATE") {
|
||||||
|
if (!cfg.switches?.enableMU || ev.otherPluginBypass) return;
|
||||||
|
const msg = ev.message;
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
// A MESSAGE_UPDATE is ONLY a real content edit if edited_timestamp is present and not synthetic
|
||||||
|
if (!msg.edited_timestamp || msg.edited_timestamp === "invalid_timestamp") return;
|
||||||
|
|
||||||
|
const chId = msg.channel_id || ev.channelId;
|
||||||
|
const id = msg.id || ev.id;
|
||||||
|
if (!chId || !id) return;
|
||||||
|
|
||||||
|
const orig = MessageStore?.getMessage?.(chId, id) || ChannelMessages?.get?.(chId)?.get?.(id);
|
||||||
|
if (!orig?.author?.id) return;
|
||||||
|
|
||||||
|
if (cfg.switches.ignoreBots && (orig.author.bot || orig.author.isNonUserBot?.())) return;
|
||||||
|
if (cfg.switches.ignoreOwnMessages && orig.author.id === currentUserId) return;
|
||||||
|
|
||||||
|
if (!orig.content || !msg.content || msg.content === orig.content) return;
|
||||||
|
|
||||||
|
if (cfg.inputs?.ignoredUserList?.length) {
|
||||||
|
if (cfg.inputs.ignoredUserList.some((u) => u.id === orig.author.id || u.username === orig.author.username)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editedTag = cfg.inputs?.editedMessageBuffer || "`[ EDITED ]`";
|
||||||
|
if (orig.content.includes(editedTag) && orig.content.endsWith(msg.content)) return;
|
||||||
|
|
||||||
|
const time = cfg.switches?.addTimestampForEdits ? `(<t:${Math.floor(now() / 1000)}:${tsStyle()}>)` : null;
|
||||||
|
const tsPos = cfg.misc?.timestampPos === "BEFORE";
|
||||||
|
|
||||||
|
let prefix = editedTag;
|
||||||
|
prefix = time ? (tsPos ? `${time} ${prefix}\n\n` : `${prefix} ${time}\n\n`) : `${prefix}\n\n`;
|
||||||
|
|
||||||
|
const gatewayOrig = recordToGateway(orig);
|
||||||
|
|
||||||
|
ev.message = {
|
||||||
|
...gatewayOrig,
|
||||||
|
...msg,
|
||||||
|
content: `${orig.content} ${prefix}${msg.content}`,
|
||||||
|
guild_id: ChannelStore?.getChannel?.(chId)?.guild_id ?? msg.guild_id,
|
||||||
|
edited_timestamp: "invalid_timestamp",
|
||||||
|
message_reference: msg?.message_reference || orig?.messageReference || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("[ANTIED] Flux dispatch error:", e?.message ?? e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
40
plugins/antied/src/patches/messageRecord.ts
Normal file
40
plugins/antied/src/patches/messageRecord.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { after } from "@vendetta/patcher";
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
|
||||||
|
const MessageRecordUtils = findByProps("updateMessageRecord", "createMessageRecord");
|
||||||
|
|
||||||
|
export function patchMessageRecord(isEnabledRef: { current: boolean }): () => void {
|
||||||
|
if (!MessageRecordUtils) return () => {};
|
||||||
|
|
||||||
|
const cleanups: (() => void)[] = [];
|
||||||
|
|
||||||
|
cleanups.push(
|
||||||
|
after("createMessageRecord", MessageRecordUtils, function ([message], record) {
|
||||||
|
if (isEnabledRef.current && (message?.was_deleted || record?.was_deleted)) {
|
||||||
|
if (record) record.was_deleted = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
cleanups.push(
|
||||||
|
after("updateMessageRecord", MessageRecordUtils, function ([oldRecord, newRecord], record) {
|
||||||
|
if (isEnabledRef.current && (oldRecord?.was_deleted || newRecord?.was_deleted)) {
|
||||||
|
if (record) record.was_deleted = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof MessageRecordUtils.updateServerMessage === "function") {
|
||||||
|
cleanups.push(
|
||||||
|
after("updateServerMessage", MessageRecordUtils, function ([oldRecord, newRecord], record) {
|
||||||
|
if (isEnabledRef.current && (oldRecord?.was_deleted || newRecord?.was_deleted)) {
|
||||||
|
if (record) record.was_deleted = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const fn of cleanups) fn();
|
||||||
|
};
|
||||||
|
}
|
||||||
30
plugins/antied/src/patches/selfEdit.ts
Normal file
30
plugins/antied/src/patches/selfEdit.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { before } from "@vendetta/patcher";
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { storage } from "@vendetta/plugin";
|
||||||
|
import { regexEscaper } from "./actionSheet";
|
||||||
|
import type { AntiedStorage } from "../lib/types";
|
||||||
|
|
||||||
|
const Message = findByProps("sendMessage", "startEditMessage");
|
||||||
|
|
||||||
|
export function patchSelfEdit(isEnabledRef: { current: boolean }): () => void {
|
||||||
|
if (!Message?.startEditMessage) return () => {};
|
||||||
|
|
||||||
|
return before("startEditMessage", Message, (args: any[]) => {
|
||||||
|
if (!isEnabledRef.current || !args?.length) return;
|
||||||
|
|
||||||
|
const cfg = storage as AntiedStorage;
|
||||||
|
const edited = cfg.inputs?.editedMessageBuffer || "`[ EDITED ]`";
|
||||||
|
const dan = regexEscaper(edited);
|
||||||
|
|
||||||
|
const regexPattern = new RegExp(
|
||||||
|
`(?:(?:\\s${dan}(\\s\\(<t:\\d+:[tTdDfFR]>\\))?\\n{2})|(?:(?:\\s\\(<t:\\d+:[tTdDfFR]>\\) ${dan}\\n{2})))`,
|
||||||
|
"gm"
|
||||||
|
);
|
||||||
|
|
||||||
|
const msg = args[2];
|
||||||
|
if (typeof msg === "string") {
|
||||||
|
const lats = msg.split(regexPattern);
|
||||||
|
args[2] = lats[lats.length - 1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
114
plugins/antied/src/patches/updateRows.ts
Normal file
114
plugins/antied/src/patches/updateRows.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { ReactNative } from "@vendetta/metro/common";
|
||||||
|
import { before, after } from "@vendetta/patcher";
|
||||||
|
import { storage } from "@vendetta/plugin";
|
||||||
|
import { findByProps, findByName } from "@vendetta/metro";
|
||||||
|
import { rawFind } from "@shared/lib/rawFind";
|
||||||
|
import { waitFor } from "@shared/lib/waitFor";
|
||||||
|
import type { AntiedStorage } from "../lib/types";
|
||||||
|
|
||||||
|
const toHex = (v: string | undefined, fallback: string) => {
|
||||||
|
const s = String(v || "").trim();
|
||||||
|
const hex = s.startsWith("#") ? s.slice(1) : s;
|
||||||
|
return /^[0-9a-fA-F]{6}$/.test(hex) ? `#${hex.toUpperCase()}` : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleRow(row: any, deletedMessagesMap: Map<string, any>) {
|
||||||
|
if (!row || row.type !== 1) return;
|
||||||
|
const msg = row.message;
|
||||||
|
if (!msg || (!deletedMessagesMap.has(msg.id) && !msg.was_deleted)) return;
|
||||||
|
|
||||||
|
const cfg = storage as AntiedStorage;
|
||||||
|
const {
|
||||||
|
colors: { textColor, backgroundColor, backgroundColorAlpha, gutterColor, gutterColorAlpha },
|
||||||
|
switches: { useBackgroundColor, minimalistic },
|
||||||
|
inputs: { deletedMessageBuffer },
|
||||||
|
} = cfg;
|
||||||
|
|
||||||
|
msg.edited = deletedMessageBuffer || "This message is deleted";
|
||||||
|
|
||||||
|
if (!minimalistic) {
|
||||||
|
msg.textColor = ReactNative.processColor(toHex(textColor, "#E40303"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!minimalistic && useBackgroundColor) {
|
||||||
|
row.backgroundHighlight = {
|
||||||
|
backgroundColor: ReactNative.processColor(toHex(backgroundColor, "#FF2C2F") + backgroundColorAlpha),
|
||||||
|
gutterColor: ReactNative.processColor(toHex(gutterColor, "#FF2C2F") + gutterColorAlpha),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNativeUpdateRows(m: any): boolean {
|
||||||
|
return typeof m?.updateRows === "function" && m.updateRows.toString().includes("[native code]");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchUpdateRows(deletedMessagesMap: Map<string, any>, isEnabledRef: { current: boolean }): () => void {
|
||||||
|
const cleanups: (() => void)[] = [];
|
||||||
|
|
||||||
|
const applyHook = (target: any) => {
|
||||||
|
cleanups.push(
|
||||||
|
before("updateRows", target, (args: any[]) => {
|
||||||
|
if (!isEnabledRef.current || !args?.length) return;
|
||||||
|
if (!deletedMessagesMap.size) return;
|
||||||
|
|
||||||
|
const raw = args[1];
|
||||||
|
if (!raw) return;
|
||||||
|
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
try {
|
||||||
|
const rows = JSON.parse(raw);
|
||||||
|
let mutated = false;
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row?.type === 1 && row?.message && (deletedMessagesMap.has(row.message.id) || row.message.was_deleted)) {
|
||||||
|
handleRow(row, deletedMessagesMap);
|
||||||
|
mutated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mutated) {
|
||||||
|
args[1] = JSON.stringify(rows);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(raw)) {
|
||||||
|
for (const row of raw) handleRow(row, deletedMessagesMap);
|
||||||
|
} else if (raw && typeof raw === "object" && Array.isArray(raw.rows)) {
|
||||||
|
for (const row of raw.rows) handleRow(row, deletedMessagesMap);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { NativeModules } = ReactNative;
|
||||||
|
const DCDChatManager = NativeModules?.DCDChatManager;
|
||||||
|
if (DCDChatManager?.updateRows) {
|
||||||
|
applyHook(DCDChatManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeChat = rawFind<any>(isNativeUpdateRows) || findByProps("updateRows", "getConstants") || findByProps("updateRows");
|
||||||
|
if (nativeChat && nativeChat !== DCDChatManager) {
|
||||||
|
applyHook(nativeChat);
|
||||||
|
} else if (!DCDChatManager) {
|
||||||
|
const handle = waitFor(
|
||||||
|
() => rawFind<any>(isNativeUpdateRows) || findByProps("updateRows"),
|
||||||
|
(target) => applyHook(target)
|
||||||
|
);
|
||||||
|
cleanups.push(() => handle.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
const RowManager = findByName("RowManager", false) || findByProps("RowManager")?.RowManager;
|
||||||
|
if (RowManager?.prototype?.generate) {
|
||||||
|
cleanups.push(
|
||||||
|
after("generate", RowManager.prototype, (_args: any[], rowObj: any) => {
|
||||||
|
if (!isEnabledRef.current || !deletedMessagesMap.size) return;
|
||||||
|
const row = rowObj?.row || rowObj;
|
||||||
|
handleRow(row, deletedMessagesMap);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const fn of cleanups) fn();
|
||||||
|
};
|
||||||
|
}
|
||||||
169
plugins/antied/src/ui/Settings.tsx
Normal file
169
plugins/antied/src/ui/Settings.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
import { findByStoreName } from "@vendetta/metro";
|
||||||
|
import { storage } from "@vendetta/plugin";
|
||||||
|
import { useProxy } from "@vendetta/storage";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
|
||||||
|
import SettingsScaffold from "@shared/ui/SettingsScaffold";
|
||||||
|
import NoteBox from "@shared/ui/NoteBox";
|
||||||
|
import ColorInput from "@shared/ui/ColorInput";
|
||||||
|
import { TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@shared/ui/table";
|
||||||
|
import type { AntiedStorage } from "../lib/types";
|
||||||
|
|
||||||
|
const UserStore = findByStoreName("UserStore");
|
||||||
|
const { View, Text, TouchableOpacity, Image } = RN;
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
useProxy(storage);
|
||||||
|
const cfg = storage as AntiedStorage;
|
||||||
|
|
||||||
|
cfg.switches ??= {} as any;
|
||||||
|
cfg.inputs ??= {} as any;
|
||||||
|
cfg.colors ??= {} as any;
|
||||||
|
cfg.misc ??= {} as any;
|
||||||
|
|
||||||
|
const ignoredUsers = cfg.inputs.ignoredUserList || [];
|
||||||
|
|
||||||
|
const handleRemoveUser = (id: string) => {
|
||||||
|
cfg.inputs.ignoredUserList = ignoredUsers.filter((u) => u.id !== id);
|
||||||
|
showToast("User removed from ignore list", getAssetIDByName("Check"));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsScaffold>
|
||||||
|
<NoteBox>
|
||||||
|
Cute Moodle (Antied) keeps deleted messages and edit history visible in your chat with customizable highlights, indicators, and long-press actionsheet tools.
|
||||||
|
</NoteBox>
|
||||||
|
|
||||||
|
<TableRowGroup title="General Logger">
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Log deleted messages"
|
||||||
|
subLabel="Keep deleted messages visible in chat"
|
||||||
|
value={!!cfg.switches.enableMD}
|
||||||
|
onValueChange={(v: boolean) => {
|
||||||
|
cfg.switches.enableMD = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Log edited messages"
|
||||||
|
subLabel="Track edits and original message contents"
|
||||||
|
value={!!cfg.switches.enableMU}
|
||||||
|
onValueChange={(v: boolean) => {
|
||||||
|
cfg.switches.enableMU = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Ignore bots"
|
||||||
|
subLabel="Do not log messages or edits from bot accounts"
|
||||||
|
value={!!cfg.switches.ignoreBots}
|
||||||
|
onValueChange={(v: boolean) => {
|
||||||
|
cfg.switches.ignoreBots = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Ignore your own messages"
|
||||||
|
subLabel="Do not log your own deletions and edits"
|
||||||
|
value={!!cfg.switches.ignoreOwnMessages}
|
||||||
|
onValueChange={(v: boolean) => {
|
||||||
|
cfg.switches.ignoreOwnMessages = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableRowGroup>
|
||||||
|
|
||||||
|
<TableRowGroup title="Styling & Appearance">
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Minimalistic mode"
|
||||||
|
subLabel="Show simple indicators without heavy colored highlights"
|
||||||
|
value={!!cfg.switches.minimalistic}
|
||||||
|
onValueChange={(v: boolean) => {
|
||||||
|
cfg.switches.minimalistic = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Use background highlight"
|
||||||
|
subLabel="Apply colored tint to deleted message backgrounds"
|
||||||
|
value={!!cfg.switches.useBackgroundColor}
|
||||||
|
onValueChange={(v: boolean) => {
|
||||||
|
cfg.switches.useBackgroundColor = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!cfg.switches.minimalistic && (
|
||||||
|
<>
|
||||||
|
<ColorInput
|
||||||
|
label="Text color"
|
||||||
|
value={cfg.colors.textColor || "#E40303"}
|
||||||
|
defaultValue="#E40303"
|
||||||
|
onChange={(hex: string) => {
|
||||||
|
cfg.colors.textColor = hex;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{cfg.switches.useBackgroundColor && (
|
||||||
|
<ColorInput
|
||||||
|
label="Background highlight color"
|
||||||
|
value={cfg.colors.backgroundColor || "#FF2C2F"}
|
||||||
|
defaultValue="#FF2C2F"
|
||||||
|
onChange={(hex: string) => {
|
||||||
|
cfg.colors.backgroundColor = hex;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Add timestamp for edits"
|
||||||
|
subLabel="Include relative timestamp on edited versions"
|
||||||
|
value={!!cfg.switches.addTimestampForEdits}
|
||||||
|
onValueChange={(v: boolean) => {
|
||||||
|
cfg.switches.addTimestampForEdits = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableRowGroup>
|
||||||
|
|
||||||
|
<TableRowGroup title="Custom Buffers">
|
||||||
|
<TextInput
|
||||||
|
label="Deleted indicator text"
|
||||||
|
value={cfg.inputs.deletedMessageBuffer || "This message is deleted"}
|
||||||
|
onChange={(v: string) => {
|
||||||
|
cfg.inputs.deletedMessageBuffer = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Edited prefix tag"
|
||||||
|
value={cfg.inputs.editedMessageBuffer || "`[ EDITED ]`"}
|
||||||
|
onChange={(v: string) => {
|
||||||
|
cfg.inputs.editedMessageBuffer = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableRowGroup>
|
||||||
|
|
||||||
|
<TableRowGroup title={`Ignored Users (${ignoredUsers.length})`}>
|
||||||
|
{ignoredUsers.length === 0 ? (
|
||||||
|
<View style={{ padding: 16 }}>
|
||||||
|
<Text style={{ opacity: 0.6, fontSize: 13, color: "#96989d" }}>No users currently ignored.</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
ignoredUsers.map((u) => {
|
||||||
|
const user = UserStore?.getUser?.(u.id);
|
||||||
|
const name = user?.username ? `@${user.username}` : u.username || u.id;
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={u.id}
|
||||||
|
label={name}
|
||||||
|
subLabel={`ID: ${u.id}`}
|
||||||
|
trailing={
|
||||||
|
<TouchableOpacity onPress={() => handleRemoveUser(u.id)}>
|
||||||
|
<Image
|
||||||
|
source={getAssetIDByName("TrashIcon") || getAssetIDByName("ic_trash_24px")}
|
||||||
|
style={{ width: 20, height: 20, tintColor: "#ff4d4d" }}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableRowGroup>
|
||||||
|
</SettingsScaffold>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
import { applyPatches } from "@shared/lib/patcher";
|
import { applyPatches } from "@shared/lib/patcher";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import patchRowManager from "./patches/rowManager";
|
import patchRowManager from "./patches/rowManager";
|
||||||
import patchTimestamp from "./patches/timestamp";
|
import patchTimestamp from "./patches/timestamp";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
@ -9,6 +10,8 @@ let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
storage.selected ??= "calendar";
|
storage.selected ??= "calendar";
|
||||||
storage.customFormat ??= "dddd, MMMM Do YYYY, h:mm:ss a";
|
storage.customFormat ??= "dddd, MMMM Do YYYY, h:mm:ss a";
|
||||||
storage.separateMessages ??= false;
|
storage.separateMessages ??= false;
|
||||||
|
|
|
||||||
|
|
@ -115,10 +115,15 @@ function patchProfileRoleItem(): () => void {
|
||||||
return () => cleanups.forEach((fn) => fn());
|
return () => cleanups.forEach((fn) => fn());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
|
|
||||||
let unpatchAll: () => void = () => {};
|
let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
unpatchAll = applyPatches("CopyRoleColor", logger, {
|
unpatchAll = applyPatches("CopyRoleColor", logger, {
|
||||||
"role pill long-press": patchRolePill,
|
"role pill long-press": patchRolePill,
|
||||||
"profile role item long-press": patchProfileRoleItem
|
"profile role item long-press": patchProfileRoleItem
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
import { applyPatches } from "@shared/lib/patcher";
|
import { applyPatches } from "@shared/lib/patcher";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import patchChat from "./patches/chat";
|
import patchChat from "./patches/chat";
|
||||||
import patchName from "./patches/name";
|
import patchName from "./patches/name";
|
||||||
import patchDetails from "./patches/details";
|
import patchDetails from "./patches/details";
|
||||||
|
|
@ -12,6 +13,8 @@ let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
storage.tags ??= {};
|
storage.tags ??= {};
|
||||||
|
|
||||||
unpatchAll = applyPatches("Custom User Tags", logger, {
|
unpatchAll = applyPatches("Custom User Tags", logger, {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
import { applyPatches } from "@shared/lib/patcher";
|
import { applyPatches } from "@shared/lib/patcher";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import { FluxDispatcher } from "@fpte/lib/flux";
|
import { FluxDispatcher } from "@fpte/lib/flux";
|
||||||
import { UserProfileStore, UserStore } from "@fpte/lib/stores";
|
import { UserProfileStore, UserStore } from "@fpte/lib/stores";
|
||||||
import {
|
import {
|
||||||
|
|
@ -31,6 +32,8 @@ let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
// The "real" picker chains Discord's own hooks onto this fiber outside React's normal
|
// The "real" picker chains Discord's own hooks onto this fiber outside React's normal
|
||||||
// reconciliation, risking a "Rendered more/fewer hooks" crash - default to the safer,
|
// reconciliation, risking a "Rendered more/fewer hooks" crash - default to the safer,
|
||||||
// self-contained fallback picker instead.
|
// self-contained fallback picker instead.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
onLoad: () => {
|
||||||
|
enforceBlacklistOnLoad(id);
|
||||||
|
},
|
||||||
settings: Settings
|
settings: Settings
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
import { patchFluxIntercept, rehydrateFromLog, revertFakedMessages } from "./patches/fluxIntercept";
|
import { patchFluxIntercept, rehydrateFromLog, revertFakedMessages } from "./patches/fluxIntercept";
|
||||||
import { patchRowStyling } from "./patches/rowStyling";
|
import { patchRowStyling } from "./patches/rowStyling";
|
||||||
|
|
@ -29,6 +30,8 @@ function initStorage() {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
initStorage();
|
initStorage();
|
||||||
console.log(TAG, "onLoad");
|
console.log(TAG, "onLoad");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import loadCommands from "./commands";
|
import loadCommands from "./commands";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
||||||
|
|
@ -5,6 +7,7 @@ let unregisterFns: (() => void)[] = [];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
unregisterFns = loadCommands();
|
unregisterFns = loadCommands();
|
||||||
},
|
},
|
||||||
onUnload: () => unregisterFns.forEach((fn) => fn()),
|
onUnload: () => unregisterFns.forEach((fn) => fn()),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
import patchMoreAltsSection, { SectionRow } from "./patches/sidebarPin";
|
import patchMoreAltsSection, { SectionRow } from "./patches/sidebarPin";
|
||||||
import patchContextMenu from "./patches/contextMenu";
|
import patchContextMenu from "./patches/contextMenu";
|
||||||
|
|
@ -28,6 +30,7 @@ export default {
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
ensureStorage();
|
ensureStorage();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
import { after } from "@vendetta/patcher";
|
import { after } from "@vendetta/patcher";
|
||||||
import { findInReactTree } from "@vendetta/utils";
|
import { findInReactTree } from "@vendetta/utils";
|
||||||
import { rawFindByTypeName } from "@shared/lib/rawFind";
|
import { rawFindByTypeName } from "@shared/lib/rawFind";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import PronounSection from "./ui/PronounSection";
|
import PronounSection from "./ui/PronounSection";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
||||||
|
|
@ -68,6 +70,7 @@ function attempt() {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
attempt();
|
attempt();
|
||||||
if (!patched) {
|
if (!patched) {
|
||||||
let ticks = 0;
|
let ticks = 0;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
import { applyPatches } from "@shared/lib/patcher";
|
import { applyPatches } from "@shared/lib/patcher";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import patchRing from "./patches/ringPatch";
|
import patchRing from "./patches/ringPatch";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
||||||
|
|
@ -8,6 +9,8 @@ let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
// Force re-enabling once, superseding the old force-disable flag from before the growth
|
// Force re-enabling once, superseding the old force-disable flag from before the growth
|
||||||
// model in ringPatch.ts was fixed to additive px instead of a percentage multiplier.
|
// model in ringPatch.ts was fixed to additive px instead of a percentage multiplier.
|
||||||
if (storage.forceReenabledV3 !== true) {
|
if (storage.forceReenabledV3 !== true) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import loadCommands from "./commands";
|
import loadCommands from "./commands";
|
||||||
import { startScheduler, stopScheduler } from "./lib/reminders";
|
import { startScheduler, stopScheduler } from "./lib/reminders";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
@ -6,6 +8,7 @@ let unregisterFns: (() => void)[] = [];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
unregisterFns = loadCommands();
|
unregisterFns = loadCommands();
|
||||||
startScheduler();
|
startScheduler();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import loadCommands from "./commands";
|
import loadCommands from "./commands";
|
||||||
|
|
||||||
let unregisterFns: (() => void)[] = [];
|
let unregisterFns: (() => void)[] = [];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
unregisterFns = loadCommands();
|
unregisterFns = loadCommands();
|
||||||
},
|
},
|
||||||
onUnload: () => unregisterFns.forEach((fn) => fn())
|
onUnload: () => unregisterFns.forEach((fn) => fn())
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
import { applyPatches } from "@shared/lib/patcher";
|
import { applyPatches } from "@shared/lib/patcher";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import patchRows from "./patches/rows";
|
import patchRows from "./patches/rows";
|
||||||
import patchMemberList from "./patches/memberList";
|
import patchMemberList from "./patches/memberList";
|
||||||
import patchTypingWrapper from "./patches/typingWrapper";
|
import patchTypingWrapper from "./patches/typingWrapper";
|
||||||
|
|
@ -11,6 +12,7 @@ let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
storage.noMention ??= false;
|
storage.noMention ??= false;
|
||||||
storage.noRole ??= false;
|
storage.noRole ??= false;
|
||||||
storage.noVoice ??= false;
|
storage.noVoice ??= false;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
import PluginsBrowser from "./ui/PluginsBrowser";
|
import PluginsBrowser from "./ui/PluginsBrowser";
|
||||||
import patchRosiesPlugsSection, { SectionRow } from "./patches/settings";
|
import patchRosiesPlugsSection, { SectionRow } from "./patches/settings";
|
||||||
|
|
@ -38,6 +40,7 @@ function buildRows(): SectionRow[] {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
try {
|
try {
|
||||||
unpatch = patchRosiesPlugsSection(buildRows);
|
unpatch = patchRosiesPlugsSection(buildRows);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import modules from "./modules";
|
import modules from "./modules";
|
||||||
import { vstorage } from "./lib/Module";
|
import { vstorage } from "./lib/Module";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
vstorage.modules ??= {};
|
vstorage.modules ??= {};
|
||||||
|
|
||||||
for (const module of modules) {
|
for (const module of modules) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||||
import { instead } from "@vendetta/patcher";
|
import { instead } from "@vendetta/patcher";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import { intoChunks } from "./lib/split";
|
import { intoChunks } from "./lib/split";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
||||||
|
|
@ -18,6 +19,7 @@ let unpatchEdit: (() => boolean) | undefined;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
storage.splitOnWords ??= false;
|
storage.splitOnWords ??= false;
|
||||||
|
|
||||||
const MaxLengthModule = findByProps("getMaxMessageLength");
|
const MaxLengthModule = findByProps("getMaxMessageLength");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
import { applyPatches } from "@shared/lib/patcher";
|
import { applyPatches } from "@shared/lib/patcher";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import patchChat from "./patches/chat";
|
import patchChat from "./patches/chat";
|
||||||
import patchDetails from "./patches/details";
|
import patchDetails from "./patches/details";
|
||||||
import patchName from "./patches/name";
|
import patchName from "./patches/name";
|
||||||
|
|
@ -12,6 +13,7 @@ let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
storage.useRoleColor ??= false;
|
storage.useRoleColor ??= false;
|
||||||
storage.tags ??= {};
|
storage.tags ??= {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
import { applyPatches } from "@shared/lib/patcher";
|
import { applyPatches } from "@shared/lib/patcher";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import patchTypingIndicator from "./patches/typingIndicator";
|
import patchTypingIndicator from "./patches/typingIndicator";
|
||||||
|
|
||||||
let unpatchAll: () => void = () => {};
|
let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
unpatchAll = applyPatches("TypingAvatars", logger, {
|
unpatchAll = applyPatches("TypingAvatars", logger, {
|
||||||
"typing indicator avatars": patchTypingIndicator
|
"typing indicator avatars": patchTypingIndicator
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import loadCommands from "./commands";
|
import loadCommands from "./commands";
|
||||||
|
|
||||||
let unregisterFns: (() => void)[] = [];
|
let unregisterFns: (() => void)[] = [];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
unregisterFns = loadCommands();
|
unregisterFns = loadCommands();
|
||||||
},
|
},
|
||||||
onUnload: () => unregisterFns.forEach((fn) => fn())
|
onUnload: () => unregisterFns.forEach((fn) => fn())
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
// Forked from bwlok/revenge-plugins (plugins/ViewRaw), GPLv3. Original authors: sapphire, Vendicated, Bwlok.
|
// Forked from bwlok/revenge-plugins (plugins/ViewRaw), GPLv3. Original authors: sapphire, Vendicated, Bwlok.
|
||||||
// https://github.com/bwlok/revenge-plugins/tree/master/plugins/ViewRaw
|
// https://github.com/bwlok/revenge-plugins/tree/master/plugins/ViewRaw
|
||||||
import { before, after } from "@vendetta/patcher";
|
import { before, after } from "@vendetta/patcher";
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
import { findInReactTree } from "@vendetta/utils";
|
import { findInReactTree } from "@vendetta/utils";
|
||||||
import { findByName, findByProps } from "@vendetta/metro";
|
import { findByName, findByProps } from "@vendetta/metro";
|
||||||
import { React } from "@vendetta/metro/common";
|
import { React } from "@vendetta/metro/common";
|
||||||
import { Forms } from "@vendetta/ui/components";
|
import { Forms } from "@vendetta/ui/components";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import RawPage from "./RawPage";
|
import RawPage from "./RawPage";
|
||||||
import { recordDetection } from "./lib/diagnostics";
|
import { recordDetection } from "./lib/diagnostics";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
@ -66,97 +68,101 @@ function actionSheetRowButton(navigator: () => JSX.Element) {
|
||||||
|
|
||||||
const patches: (() => void)[] = [];
|
const patches: (() => void)[] = [];
|
||||||
|
|
||||||
const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
|
export default {
|
||||||
if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
|
|
||||||
component.then((instance: any) => {
|
const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
|
||||||
// Discord reuses the same lazy-loaded instance across opens - patch once, track the active
|
if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
|
||||||
// message on the instance itself.
|
|
||||||
instance.__viewRawActiveMessage = msg.message;
|
|
||||||
if (instance.__viewRawPatched) return;
|
|
||||||
instance.__viewRawPatched = true;
|
|
||||||
|
|
||||||
const unpatchDefault = after("default", instance, (_: any, component: any) => {
|
component.then((instance: any) => {
|
||||||
try {
|
instance.__viewRawActiveMessage = msg.message;
|
||||||
if (!Navigator) {
|
if (instance.__viewRawPatched) return;
|
||||||
console.log("[ViewRaw] Error: Navigator not found - can't open RawPage");
|
instance.__viewRawPatched = true;
|
||||||
recordDetection("none", "Navigator not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigator = buildNavigator(() => instance.__viewRawActiveMessage);
|
const unpatchDefault = after("default", instance, (_: any, component: any) => {
|
||||||
|
try {
|
||||||
// Strategy 1: a plain list of buttons (older/simpler layouts).
|
if (!Navigator) {
|
||||||
const buttons = findInReactTree(component, (x) => x?.[0]?.type?.name === "ButtonRow");
|
console.log("[ViewRaw] Error: Navigator not found - can't open RawPage");
|
||||||
if (buttons) {
|
recordDetection("none", "Navigator not found");
|
||||||
buttons.push(formRowButton(navigator));
|
return;
|
||||||
recordDetection("buttons");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 2: real ActionSheetRow groups, falling back to a new group if none match.
|
|
||||||
const groups = findInReactTree(
|
|
||||||
component,
|
|
||||||
(x) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Array.isArray(groups) && groups.length && ActionSheetRow) {
|
|
||||||
const button = actionSheetRowButton(navigator);
|
|
||||||
let inserted = false;
|
|
||||||
|
|
||||||
for (const group of groups) {
|
|
||||||
const children = findInReactTree(
|
|
||||||
group,
|
|
||||||
(c) => Array.isArray(c) && c.some((child: any) => child?.type?.name === "ActionSheetRow"),
|
|
||||||
);
|
|
||||||
if (Array.isArray(children)) {
|
|
||||||
children.push(button);
|
|
||||||
inserted = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navigator = buildNavigator(() => instance.__viewRawActiveMessage);
|
||||||
|
|
||||||
|
// Strategy 1: a plain list of buttons (older/simpler layouts).
|
||||||
|
const buttons = findInReactTree(component, (x) => x?.[0]?.type?.name === "ButtonRow");
|
||||||
|
if (buttons) {
|
||||||
|
buttons.push(formRowButton(navigator));
|
||||||
|
recordDetection("buttons");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: real ActionSheetRow groups, falling back to a new group if none match.
|
||||||
|
const groups = findInReactTree(
|
||||||
|
component,
|
||||||
|
(x) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(groups) && groups.length && ActionSheetRow) {
|
||||||
|
const button = actionSheetRowButton(navigator);
|
||||||
|
let inserted = false;
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
const children = findInReactTree(
|
||||||
|
group,
|
||||||
|
(c) => Array.isArray(c) && c.some((child: any) => child?.type?.name === "ActionSheetRow"),
|
||||||
|
);
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
children.push(button);
|
||||||
|
inserted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inserted && typeof groups.unshift === "function" && ActionSheetRow.Group) {
|
||||||
|
groups.unshift(<ActionSheetRow.Group>{button}</ActionSheetRow.Group>);
|
||||||
|
inserted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inserted) {
|
||||||
|
recordDetection("actionSheetRow");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: name-independent fallback matching on shape (label + onPress) instead.
|
||||||
|
const genericRowGroup = findInReactTree(
|
||||||
|
component,
|
||||||
|
(x) =>
|
||||||
|
Array.isArray(x) &&
|
||||||
|
x.length > 0 &&
|
||||||
|
x.every((el: any) => typeof el?.props?.label === "string" && typeof el?.props?.onPress === "function"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(genericRowGroup)) {
|
||||||
|
genericRowGroup.push(formRowButton(navigator));
|
||||||
|
recordDetection("generic");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[ViewRaw] Error: Could not find ActionSheet");
|
||||||
|
recordDetection("none");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[ViewRaw] Error: Failed to add View Raw button", e);
|
||||||
|
recordDetection("none", String(e));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!inserted && typeof groups.unshift === "function" && ActionSheetRow.Group) {
|
patches.push(unpatchDefault);
|
||||||
groups.unshift(<ActionSheetRow.Group>{button}</ActionSheetRow.Group>);
|
});
|
||||||
inserted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inserted) {
|
|
||||||
recordDetection("actionSheetRow");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 3: name-independent fallback matching on shape (label + onPress) instead.
|
|
||||||
const genericRowGroup = findInReactTree(
|
|
||||||
component,
|
|
||||||
(x) =>
|
|
||||||
Array.isArray(x) &&
|
|
||||||
x.length > 0 &&
|
|
||||||
x.every((el: any) => typeof el?.props?.label === "string" && typeof el?.props?.onPress === "function"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Array.isArray(genericRowGroup)) {
|
|
||||||
genericRowGroup.push(formRowButton(navigator));
|
|
||||||
recordDetection("generic");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[ViewRaw] Error: Could not find ActionSheet");
|
|
||||||
recordDetection("none");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("[ViewRaw] Error: Failed to add View Raw button", e);
|
|
||||||
recordDetection("none", String(e));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
patches.push(unpatchDefault);
|
patches.push(unpatchOpenLazy);
|
||||||
});
|
},
|
||||||
});
|
onUnload: () => {
|
||||||
|
patches.forEach((unpatch) => unpatch());
|
||||||
patches.push(unpatchOpenLazy);
|
patches.length = 0;
|
||||||
|
},
|
||||||
export default {
|
|
||||||
onUnload: () => patches.forEach((unpatch) => unpatch()),
|
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage, id } from "@vendetta/plugin";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import patchYouBarButtons from "./patches/youBarButtons";
|
import patchYouBarButtons from "./patches/youBarButtons";
|
||||||
import { setInboxTracking } from "./lib/notifications";
|
import { setInboxTracking } from "./lib/notifications";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
@ -33,6 +34,7 @@ function attempt() {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
storage.showDMButton ??= false;
|
storage.showDMButton ??= false;
|
||||||
storage.showSettingsButton ??= true;
|
storage.showSettingsButton ??= true;
|
||||||
storage.showInboxButton ??= false;
|
storage.showInboxButton ??= false;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { logger } from "@vendetta";
|
import { logger } from "@vendetta";
|
||||||
|
import { id } from "@vendetta/plugin";
|
||||||
import { applyPatches } from "@shared/lib/patcher";
|
import { applyPatches } from "@shared/lib/patcher";
|
||||||
|
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
|
||||||
import patchAutoProcess from "./patches/autoProcess";
|
import patchAutoProcess from "./patches/autoProcess";
|
||||||
import { zStorage } from "./lib/api";
|
import { zStorage } from "./lib/api";
|
||||||
import Settings from "./ui/Settings";
|
import Settings from "./ui/Settings";
|
||||||
|
|
@ -8,6 +10,7 @@ let unpatchAll: () => void = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
|
if (enforceBlacklistOnLoad(id)) return;
|
||||||
zStorage();
|
zStorage();
|
||||||
|
|
||||||
unpatchAll = applyPatches("Zipline", logger, {
|
unpatchAll = applyPatches("Zipline", logger, {
|
||||||
|
|
|
||||||
112
serve.mjs
Normal file
112
serve.mjs
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import http from "http";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
|
||||||
|
const PORT = 8000;
|
||||||
|
const DIST_DIR = path.resolve("./dist");
|
||||||
|
|
||||||
|
const MIME_TYPES = {
|
||||||
|
".html": "text/html",
|
||||||
|
".js": "application/javascript",
|
||||||
|
".json": "application/json",
|
||||||
|
".css": "text/css",
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".txt": "text/plain",
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "*");
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeUrl = decodeURIComponent(req.url?.split("?")[0] || "/");
|
||||||
|
let filePath = path.join(DIST_DIR, safeUrl);
|
||||||
|
|
||||||
|
// Root blacklist.json fallback
|
||||||
|
if (safeUrl === "/blacklist.json") {
|
||||||
|
const rootBlacklist = path.resolve("./blacklist.json");
|
||||||
|
if (fs.existsSync(rootBlacklist)) {
|
||||||
|
filePath = rootBlacklist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRIVATE_DIST_DIR = path.resolve("../revenge-plugins-private/dist");
|
||||||
|
|
||||||
|
// Direct /<plugin>/manifest.json or /<plugin>/index.js routing to install/ directory if needed
|
||||||
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||||
|
const parts = safeUrl.split("/").filter(Boolean);
|
||||||
|
if (parts.length === 2 && (parts[1] === "manifest.json" || parts[1] === "index.js")) {
|
||||||
|
const installFilePath = path.join(DIST_DIR, parts[0], "install", parts[1]);
|
||||||
|
if (fs.existsSync(installFilePath) && fs.statSync(installFilePath).isFile()) {
|
||||||
|
filePath = installFilePath;
|
||||||
|
} else {
|
||||||
|
const privateInstallFilePath = path.join(PRIVATE_DIST_DIR, parts[0], "install", parts[1]);
|
||||||
|
if (fs.existsSync(privateInstallFilePath) && fs.statSync(privateInstallFilePath).isFile()) {
|
||||||
|
filePath = privateInstallFilePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (fs.existsSync(path.join(PRIVATE_DIST_DIR, safeUrl))) {
|
||||||
|
filePath = path.join(PRIVATE_DIST_DIR, safeUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory routing:
|
||||||
|
// If requesting /<plugin>/install or /<plugin>/install/, serve manifest.json
|
||||||
|
// Otherwise serve index.html
|
||||||
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
||||||
|
if (filePath.endsWith("install") || filePath.endsWith("install" + path.sep)) {
|
||||||
|
filePath = path.join(filePath, "manifest.json");
|
||||||
|
} else {
|
||||||
|
filePath = path.join(filePath, "index.html");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||||
|
console.log(`[404] Not Found: ${req.url} -> ${filePath}`);
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("404 Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, "0.0.0.0", () => {
|
||||||
|
const interfaces = os.networkInterfaces();
|
||||||
|
const addresses = [];
|
||||||
|
for (const iface of Object.values(interfaces)) {
|
||||||
|
if (!iface) continue;
|
||||||
|
for (const alias of iface) {
|
||||||
|
if (alias.family === "IPv4" && !alias.internal) {
|
||||||
|
addresses.push(alias.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🚀 Server listening on port ${PORT} (0.0.0.0)`);
|
||||||
|
console.log(`Accessible URLs:`);
|
||||||
|
for (const addr of addresses) {
|
||||||
|
console.log(` - http://${addr}:${PORT}/`);
|
||||||
|
console.log(` • Antied: http://${addr}:${PORT}/antied/install/`);
|
||||||
|
console.log(` • Antied Zero: http://${addr}:${PORT}/antied-zero/install/`);
|
||||||
|
}
|
||||||
|
console.log(`\nReady for device testing via Tailscale / LAN!\n`);
|
||||||
|
});
|
||||||
91
shared/lib/blacklist.ts
Normal file
91
shared/lib/blacklist.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { logger, plugin } from "@vendetta";
|
||||||
|
import { findByStoreName, findByProps } from "@vendetta/metro";
|
||||||
|
import { removePlugin } from "@vendetta/plugins";
|
||||||
|
import { safeFetch } from "@vendetta/utils";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
|
||||||
|
const UserStore = findByStoreName("UserStore");
|
||||||
|
const AuthStore = findByStoreName("AuthenticationStore") || findByProps("getToken");
|
||||||
|
|
||||||
|
export const DEFAULT_BLACKLIST_URL = "https://raw.githubusercontent.com/everestmcarthur/revenge-plugins/main/blacklist.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local fallback blacklist: Any Discord User IDs hardcoded here.
|
||||||
|
*/
|
||||||
|
export const LOCAL_BLACKLIST: string[] = [];
|
||||||
|
|
||||||
|
function getCurrentUserId(): string | undefined {
|
||||||
|
return UserStore?.getCurrentUser?.()?.id || AuthStore?.getId?.() || AuthStore?.getCurrentUser?.()?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Grab list from remote url, cache it, and remove self if the user is blacklisted */
|
||||||
|
export async function fetchBlacklist(url: string = DEFAULT_BLACKLIST_URL): Promise<{ list: (string | number)[] }> {
|
||||||
|
try {
|
||||||
|
const bustUrl = url.includes("?") ? `${url}&_t=${Date.now()}` : `${url}?_t=${Date.now()}`;
|
||||||
|
const res = await safeFetch(bustUrl, { cache: "no-store" });
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
return { list: Array.isArray(data?.list) ? data.list : [] };
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.info("[Blacklist] Unable to fetch remote blacklist data:", e?.message ?? e);
|
||||||
|
}
|
||||||
|
return { list: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if user is in local or remote blacklist and trigger self-deletion if matched */
|
||||||
|
export function checkAndEnforceBlacklist(
|
||||||
|
blocklist?: { list?: (string | number)[] },
|
||||||
|
pluginId: string = plugin?.id,
|
||||||
|
delaySeconds = 15
|
||||||
|
): boolean {
|
||||||
|
const myId = getCurrentUserId();
|
||||||
|
if (!myId) return false;
|
||||||
|
|
||||||
|
// Combine local static blacklist with remote blacklist
|
||||||
|
const combined = new Set<string>([
|
||||||
|
...LOCAL_BLACKLIST.map(String),
|
||||||
|
...(blocklist?.list?.map(String) || []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!combined.size) return false;
|
||||||
|
|
||||||
|
const isBlacklisted = combined.has(String(myId));
|
||||||
|
if (isBlacklisted && pluginId) {
|
||||||
|
logger.warn(`[Blacklist] User ${myId} is blacklisted. Plugin will self-delete in ${delaySeconds}s.`);
|
||||||
|
try {
|
||||||
|
showToast(`[Blacklist] You are blacklisted from using this plugin. Self-deleting in ${delaySeconds}s.`);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.info("[Blacklist] Removing blacklisted plugin:", pluginId);
|
||||||
|
try {
|
||||||
|
removePlugin(pluginId);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error("[Blacklist] Failed to remove plugin:", e?.message ?? e);
|
||||||
|
}
|
||||||
|
}, delaySeconds * 1000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard 1-line enforcement for any plugin's onLoad lifecycle.
|
||||||
|
* Checks local blacklist immediately, then fetches remote in background.
|
||||||
|
*/
|
||||||
|
export function enforceBlacklistOnLoad(pluginId: string = plugin?.id, delaySeconds = 15): boolean {
|
||||||
|
if (checkAndEnforceBlacklist(undefined, pluginId, delaySeconds)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchBlacklist(DEFAULT_BLACKLIST_URL)
|
||||||
|
.then((blocklist) => {
|
||||||
|
checkAndEnforceBlacklist(blocklist, pluginId, delaySeconds);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.info("[Blacklist] Remote check failed:", e?.message ?? e);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
85
shared/lib/recordToGateway.ts
Normal file
85
shared/lib/recordToGateway.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
export function authorToGateway(a: any): any {
|
||||||
|
if (!a || typeof a !== "object") return a;
|
||||||
|
return {
|
||||||
|
id: a.id,
|
||||||
|
username: a.username,
|
||||||
|
discriminator: a.discriminator && a.discriminator !== "???" ? String(a.discriminator) : "0",
|
||||||
|
avatar: a.avatar ?? null,
|
||||||
|
avatar_decoration_data: a.avatarDecorationData ?? a.avatar_decoration_data ?? null,
|
||||||
|
bot: Boolean(a.bot),
|
||||||
|
global_name: a.globalName ?? a.global_name ?? a.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function embedToGateway(e: any): any {
|
||||||
|
if (!e || typeof e !== "object") return e;
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
title: e.rawTitle ?? e.title,
|
||||||
|
description: e.rawDescription ?? e.description,
|
||||||
|
fields: Array.isArray(e.fields)
|
||||||
|
? e.fields.map((f: any) => ({
|
||||||
|
name: f.rawName ?? f.name ?? "",
|
||||||
|
value: f.rawValue ?? f.value ?? "",
|
||||||
|
inline: Boolean(f.inline),
|
||||||
|
}))
|
||||||
|
: undefined,
|
||||||
|
author: e.author
|
||||||
|
? {
|
||||||
|
name: e.author.name,
|
||||||
|
url: e.author.url,
|
||||||
|
icon_url: e.author.iconURL ?? e.author.icon_url,
|
||||||
|
proxy_icon_url: e.author.iconProxyURL ?? e.author.proxy_icon_url,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
footer: e.footer
|
||||||
|
? {
|
||||||
|
text: e.footer.text,
|
||||||
|
icon_url: e.footer.iconURL ?? e.footer.icon_url,
|
||||||
|
proxy_icon_url: e.footer.iconProxyURL ?? e.footer.proxy_icon_url,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
image: e.image
|
||||||
|
? {
|
||||||
|
url: e.image.url,
|
||||||
|
proxy_url: e.image.proxyURL ?? e.image.proxy_url,
|
||||||
|
width: e.image.width,
|
||||||
|
height: e.image.height,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
thumbnail: e.thumbnail
|
||||||
|
? {
|
||||||
|
url: e.thumbnail.url,
|
||||||
|
proxy_url: e.thumbnail.proxyURL ?? e.thumbnail.proxy_url,
|
||||||
|
width: e.thumbnail.width,
|
||||||
|
height: e.thumbnail.height,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function componentToGateway(c: any): any {
|
||||||
|
if (!c || typeof c !== "object") return c;
|
||||||
|
const copy = { ...c };
|
||||||
|
if (copy.type === 3 || copy.type === "3") {
|
||||||
|
copy.custom_id = copy.customId ?? copy.custom_id ?? copy.id ?? "select";
|
||||||
|
copy.min_values = copy.minValues ?? copy.min_values ?? 1;
|
||||||
|
copy.max_values = copy.maxValues ?? copy.max_values ?? 1;
|
||||||
|
} else if (copy.type === 2 || copy.type === "2") {
|
||||||
|
copy.custom_id = copy.customId ?? copy.custom_id ?? copy.id ?? "button";
|
||||||
|
}
|
||||||
|
if (Array.isArray(copy.components)) {
|
||||||
|
copy.components = copy.components.map(componentToGateway);
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordToGateway(record: any): any {
|
||||||
|
if (!record || typeof record !== "object") return record;
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
author: authorToGateway(record.author),
|
||||||
|
embeds: Array.isArray(record.embeds) ? record.embeds.map(embedToGateway) : record.embeds,
|
||||||
|
components: Array.isArray(record.components) ? record.components.map(componentToGateway) : record.components,
|
||||||
|
};
|
||||||
|
}
|
||||||
73
shared/lib/utility.ts
Normal file
73
shared/lib/utility.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { logger } from "@vendetta";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
|
||||||
|
const { openLazy } = findByProps("openLazy", "hideActionSheet") || {};
|
||||||
|
|
||||||
|
export function makeDefaults(object: any, defaults: any) {
|
||||||
|
if (object != null && defaults != null) {
|
||||||
|
for (const key of Object.keys(defaults)) {
|
||||||
|
if (typeof defaults[key] === "object" && !Array.isArray(defaults[key]) && defaults[key] !== null) {
|
||||||
|
if (typeof object[key] !== "object" || object[key] === null) object[key] = {};
|
||||||
|
makeDefaults(object[key], defaults[key]);
|
||||||
|
} else {
|
||||||
|
object[key] ??= defaults[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openSheet(sheet: any, props?: any) {
|
||||||
|
try {
|
||||||
|
if (openLazy) {
|
||||||
|
openLazy(
|
||||||
|
Promise.resolve({ default: sheet }),
|
||||||
|
"ActionSheet",
|
||||||
|
props
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error(e?.stack ?? e);
|
||||||
|
showToast("Error opening ActionSheet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setOpacity = (hex: string, alpha: number) =>
|
||||||
|
`${hex}${Math.floor(alpha * 255).toString(16).padStart(2, "0")}`;
|
||||||
|
|
||||||
|
export const colorConverter = {
|
||||||
|
toInt(hex: string) {
|
||||||
|
hex = hex.replace(/^#/, "");
|
||||||
|
return parseInt(hex, 16);
|
||||||
|
},
|
||||||
|
toHex(integer: number) {
|
||||||
|
const hex = integer.toString(16).toUpperCase();
|
||||||
|
return "#" + hex;
|
||||||
|
},
|
||||||
|
HSLtoHEX(h: number, s: number, l: number) {
|
||||||
|
l /= 100;
|
||||||
|
const a = (s * Math.min(l, 1 - l)) / 100;
|
||||||
|
const f = (n: number) => {
|
||||||
|
const k = (n + h / 30) % 12;
|
||||||
|
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||||
|
return Math.round(255 * color).toString(16).padStart(2, "0");
|
||||||
|
};
|
||||||
|
return `#${f(0)}${f(8)}${f(4)}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function validateHex(input: string | undefined | null, defaultColor = "#000000") {
|
||||||
|
if (!input) return defaultColor;
|
||||||
|
const trimmedInput = input.trim();
|
||||||
|
if (trimmedInput.startsWith("#")) {
|
||||||
|
const hexCode = trimmedInput.slice(1);
|
||||||
|
if (/^[0-9A-Fa-f]{6}$/.test(hexCode)) {
|
||||||
|
return "#" + hexCode.toUpperCase();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (/^[0-9A-Fa-f]{6}$/.test(trimmedInput)) {
|
||||||
|
return "#" + trimmedInput.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultColor;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue