From 021e0c15fcfbdcf16cdb782a75c4467242d30afb Mon Sep 17 00:00:00 2001 From: OTKUSteyler Date: Thu, 2 Apr 2026 19:29:07 +0200 Subject: [PATCH] Add files via upload --- src/core/plugins/nodelete/index.js | 138 +++++++++++++++++++ src/core/plugins/nodelete/settings.jsx | 90 ++++++++++++ src/core/plugins/nodelete/translations.js | 158 ++++++++++++++++++++++ 3 files changed, 386 insertions(+) create mode 100644 src/core/plugins/nodelete/index.js create mode 100644 src/core/plugins/nodelete/settings.jsx create mode 100644 src/core/plugins/nodelete/translations.js diff --git a/src/core/plugins/nodelete/index.js b/src/core/plugins/nodelete/index.js new file mode 100644 index 0000000..9da4933 --- /dev/null +++ b/src/core/plugins/nodelete/index.js @@ -0,0 +1,138 @@ +import settings from "./settings.jsx"; +import * as common from "../../common"; +import { FluxDispatcher, moment } from "@vendetta/metro/common"; +import { storage } from "@vendetta/plugin"; +import { before as patchBefore } from "@vendetta/patcher"; +import { findByProps, findByStoreName } from "@vendetta/metro"; +import { findInReactTree } from "@vendetta/utils"; +import { showToast } from "@vendetta/ui/toasts"; +import { getTranslation, massive } from "./translations.js"; +//import { dispatcherPatch } from "./patches/dispatcher.js"; +//import { contextMenuPatch } from "./patches/contextMenu.js" + +common.makeDefaults(storage, { + ignore: { + users: [], + channels: [], + bots: false, + }, + timestamps: false, + ew: false, + onlyTimestamps: false, +}); +let MessageStore, + deleteable = []; + +const patches = [] +export default { + settings, + onUnload() { + for (const unpatch of patches) unpatch() + }, + onLoad() { + try { + patches.push( + patchBefore("dispatch", FluxDispatcher, (args) => { + try { + if (!MessageStore) MessageStore = findByStoreName("MessageStore"); + const event = args[0]; + + if (!event || event?.type !== "MESSAGE_DELETE") return; + if (!event?.id || !event?.channelId) return; + + const message = MessageStore.getMessage(event.channelId, event.id); + + if (storage["ignore"]["users"].includes(message?.author?.id)) return; + if (storage["ignore"]["bots"] && message?.author?.bot) return; + + if (deleteable.includes(event.id)) { + deleteable.splice(deleteable.indexOf(event.id), 1); + return args; + } + deleteable.push(event.id); + + let automodMessage = getTranslation("thisMessageWasDeleted"); + if (storage["timestamps"]) automodMessage += ` (${moment().format(storage["ew"] ? "hh:mm:ss.SS a" : "HH:mm:ss.SS")})`; + + // overwrite the message delete event with the automod edit fail one + args[0] = { + type: "MESSAGE_EDIT_FAILED_AUTOMOD", + messageData: { + type: 1, + message: { + channelId: event.channelId, + messageId: event.id, + }, + }, + errorResponseBody: { + code: 200000, + message: automodMessage, + }, + }; + return args; + } catch (e) { + console.error(e); + alert(`[Nodelete → dispatcher patch] died\n${e.stack}`); + } + }) + ); + + /* thanks fres#2400 (<@843448897737064448>) for example patch + * add ignore user button + */ + const contextMenuUnpatch = patchBefore("render", findByProps("ScrollView").View, (args) => { + try { + const a = findInReactTree(args, (r) => r.key === ".$UserProfileOverflow"); + if (!a || !a.props || a.props.sheetKey !== "UserProfileOverflow") return; + const props = a.props.content.props; + const _labels = massive.optionLabels.flatMap(Object.values); + if (props.options.some((option) => _labels.includes(option?.label))) return; + + const focusedUserId = Object.keys(a._owner.stateNode._keyChildMapping) + .find((str) => a._owner.stateNode._keyChildMapping[str] && str.match(/(?<=\$UserProfile)\d+/)) + ?.slice?.(".$UserProfile".length); + + const optionPosition = props.options.findLastIndex((option) => option.isDestructive); + if (!storage["ignore"]["users"].includes(focusedUserId)) { + props.options.splice(optionPosition + 1, 0, { + isDestructive: true, + label: getTranslation("optionLabels.0"), // START IGNORING + onPress: () => { + storage["ignore"]["users"].push(focusedUserId); + showToast(getTranslation("toastLabels.0").replaceAll("{user}", props.header.title)); + + props.hideActionSheet(); + }, + }); + } else { + props.options.splice(optionPosition + 1, 0, { + label: getTranslation("optionLabels.1"), // STOP IGNORING + onPress: () => { + storage["ignore"]["users"].splice( + storage["ignore"]["users"].findIndex((userId) => userId === focusedUserId), + 1 + ); + showToast(getTranslation("toastLabels.1").replaceAll("{user}", props.header.title)); + + props.hideActionSheet(); + }, + }); + } + } catch (e) { + console.error(e); + let successful = false; + try { + successful = contextMenuUnpatch(); + } catch (_) { + successful = false; + } + alert(`[NoDelete → context menu patch] failed. Patch ${successful ? "dis" : "en"}abled\n${e.stack}`); + } + }); + patches.push(contextMenuUnpatch); + } catch (e) { + console.error(e); + alert(`[NoDelete] dead\n${e.stack}`); + } + }, +}; diff --git a/src/core/plugins/nodelete/settings.jsx b/src/core/plugins/nodelete/settings.jsx new file mode 100644 index 0000000..89c58b7 --- /dev/null +++ b/src/core/plugins/nodelete/settings.jsx @@ -0,0 +1,90 @@ +import { React, ReactNative } from "@vendetta/metro/common"; +import { storage } from "@vendetta/plugin"; +import { useProxy } from "@vendetta/storage"; +import { showConfirmationAlert } from "@vendetta/ui/alerts"; +import { Forms } from "@vendetta/ui/components"; +import { getAssetIDByName } from "@vendetta/ui/assets"; +import { getTranslation } from "./translations.js"; +import ItemWithRemove from "../../common/ui/ItemWithRemove.jsx"; +import { findByStoreName, findByProps } from "@vendetta/metro"; +let UserStore, UncachedUserManager, Profiles; +export default (props) => { + UserStore ??= findByStoreName("UserStore"); + UncachedUserManager ??= findByProps("fetchProfile", "getUser", "setFlag"); + Profiles ??= findByProps("showUserProfile"); + async function openProfile(userId) { + const show = Profiles.showUserProfile; + + UserStore.getUser(userId) ? show({ userId }) : UncachedUserManager.getUser(userId).then(({ id }) => show({ userId: id })); + } + + useProxy(storage); + const [users, setUsers] = React.useState(storage["ignore"]["users"]); + + const handleRemoveUser = (userId) => { + const newArr = users.filter((id) => id !== userId); + storage["ignore"].users = newArr; + setUsers(newArr); + }; + const handleClearUsers = () => { + storage["ignore"].users = []; + setUsers([]); + }; + let uncached = 0; + + return ( + + + (storage.timestamps = v)} />} /> + (storage.ew = v)} />} /> + + + + + (storage["ignore"].bots = value)} />} /> + } + onPress={() => { + if (users.length !== 0) + showConfirmationAlert({ + title: getTranslation("settings.confirmClear.title"), + content: getTranslation("settings.confirmClear.description", true)?.make?.(users.length), + confirmText: getTranslation("settings.confirmClear.yes"), + cancelText: getTranslation("settings.confirmClear.no"), + confirmColor: "brand", + onConfirm: handleClearUsers, + }); + }} + /> + + {users.map((id) => { + const User = UserStore.getUser(id) ?? {}; + let pfp = User?.getAvatarURL?.(null,26)?.replace?.(/\.(gif|webp)/, ".png"); + if (!pfp) { + pfp = "https://cdn.discordapp.com/embed/avatars/1.png?size=48"; + User.username = `${id} Uncached`; + User.discriminator = "0"; + if (uncached === 0) User.username += ", press the avatar"; + uncached++; + } + + return ( + { + openProfile(id); + }} + onRemove={() => handleRemoveUser(id)} + label={User.username + (User.discriminator == 0 ? "" : `#${User.discriminator}`)} + labelRemove={getTranslation("settings.removeUserButton")} + /> + ); + })} + + + + + + ); +}; diff --git a/src/core/plugins/nodelete/translations.js b/src/core/plugins/nodelete/translations.js new file mode 100644 index 0000000..4144b7e --- /dev/null +++ b/src/core/plugins/nodelete/translations.js @@ -0,0 +1,158 @@ +function slavicNoun(number, $1, $2, $5) { + let n = Math.abs(number); + n %= 100; + if (n >= 5 && n <= 20) return $5; + + n %= 10; + if (n === 1) return $1; + if (n >= 2 && n <= 4) return $2; + return $5; +} +function latinNoun(number, $1, $x) { + return number === 1 ? $1 : $x; +} +export let massive = {}; +massive = { + settings: { + titles: { + settings: { + "en-GB": "Settings", + uk: "Налаштування", + ru: "Настройки", + }, + filters: { + "en-GB": "Filters", + uk: "Фільтри", + ru: "Фильтры", + }, + }, + showTimestamps: { + "en-GB": "Show the time of deletion", + uk: "Показувати час видалення", + ru: "Показывать время удаления", + }, + ewTimestampFormat: { + "en-GB": "Use 12-hour format", + uk: "Використовувати 12-годинний формат", + ru: "Использовать 12-часовой формат", + }, + youDeletedItWarning: { + "en-GB": "The messages YOU deleted - are not saved", + uk: "Повідомлення які видалили ВИ - не зберігаются", + ru: "Сообщения удаленные ВАМИ - не сохраняются", + }, + addUsersInfo: { + "en-GB": () => + `To add or remove users from the ignore list, follow these steps:\n` + + `1. open their profile\n` + + `2. press the •••\n` + + `3. press "${massive.optionLabels[0]["en-GB"]}"\n` + + `4. 🎉`, + uk: () => + `Щоб добавити когось до списку ігнорованих користувачів, виконайте ці дії:\n` + + `1. відкрите їх профіль\n` + + `2. натисніть •••\n` + + `3. натисніть "${massive.optionLabels[0]["uk"]}"\n` + + `4. 🎉`, + ru: () => + `Чтобы добавить кого-то в список игнорированных пользователей - следуйте этим шагам\n` + + `1. откройте их профиль\n` + + `2. нажмите •••\n` + + `3. нажмите "${massive.optionLabels[0]["ru"]}"\n` + + `4. 🎉`, + }, + ignoreBots: { + "en-GB": "Ignore bots", + uk: "Ігнорувати ботів", + ru: "Игнорировать ботов", + }, + clearUsersLabel: { + "en-GB": (amount) => + `You have ${amount} user${latinNoun(amount, "", "s")} in the ignored users list`, + uk: (amount) => + `Ви маєте ${amount} користувач${slavicNoun(amount, "а", "а", "ів")} у списку ігнорованих користувачів`, + ru: (amount) => + `Вы имеете ${amount} пользовател${slavicNoun(amount, "я", "я", "ей")} в списке игнорированных пользователей`, + }, + + confirmClear: { + title: { + "en-GB": "Hold on!", + uk: "Почекай-но!", + ru: "Положди-ка!", + }, + description: { + "en-GB": (amount) => + `This will remove ${amount} user${latinNoun(amount, "", "s")} from the ignored users list.\nDo you you really want to do that?`, + uk: (amount) => + `Це прибере ${amount} користувач${slavicNoun(amount, "а", "а", "ів")} зі списку ігнорованих користувачів.\nВи дійсно хочете продовжити?`, + ru: (amount) => + `Это уберет ${amount} пользовател${slavicNoun(amount, "я", "я", "ей")} из списка игнорированных пользователей.\nВы действительно хотите продолжить?`, + }, + yes: { + "en-GB": "Yes", + uk: "Так", + ru: "Да", + }, + no: { + "en-GB": "Cancel", + uk: "Відминити", + ru: "Отменить", + }, + }, + removeUserButton: { + "en-GB": "REMOVE", + uk: "ПРИБРАТИ", + ru: "УБРАТЬ", + }, + }, + optionLabels: [ + { + "en-GB": "Add to NoDelete ignored users list", + uk: "Добавити до списку ігнорованих користувачів у NoDelete", + ru: "Добавить в список игнорированных пользователей в NoDelete", + }, + { + "en-GB": "Remove from the NoDelete ignore list", + uk: "Прибрати з списку ігнорованих користувачів у NoDelete", + ru: "Убрать из списка игнорированных пользователей в NoDelete", + }, + ], + toastLabels: [ + { + "en-GB": "Added {user} to the ignored users list", + uk: "{user} добавлено до списку ігнорованих користувачів", + ru: "{user} добавлены в список игнорированных пользователей", + }, + { + "en-GB": "Removed {user} from the ignored users list", + uk: "{user} прибрано зі списку ігнорованих користувачів", + ru: "{user} убраны из списка игнорированных пользователей", + }, + ], + thisMessageWasDeleted: { + "en-GB": "This message was deleted", + uk: "Це повідомлення було видалено", + ru: "Это сообщение было удалено", + }, +}; + +export const locale = () => + vendetta.metro.findByStoreName("LocaleStore").locale; +const defaultLocale = "en-GB"; +export function getTranslation(find, fn) { + let value = massive; + + for (const key of find.split(".")) { + // biome-ignore lint/suspicious/noPrototypeBuiltins: meow + if (value?.hasOwnProperty(key)) { + value = value[key]; + } else { + value = massive; + } + } + if (value === massive) return find; + let localized = value[locale()] ?? value[defaultLocale] ?? find; + if (typeof localized === "function" && !fn) localized = localized(); + return fn ? { make: localized } : localized; +}