From 35ad3fa2a15f69ade07029800731f7eafa7871ad Mon Sep 17 00:00:00 2001 From: fumiichan <35658068+fumiichan@users.noreply.github.com> Date: Fri, 31 Jan 2025 22:39:08 +0900 Subject: [PATCH] Add support to hide attachments automatically This patch includes support to hide attachments with the following configuration: - Comma separated list of User IDs - Comma separated list of domains/hostnames - A toggle to enable/disable the automatic hiding This also fixes the issue: - Fixes the issue where forwarded messages with attachments are not detected --- src/plugins/hideAttachments/index.tsx | 205 +++++++++++++++++++++----- src/plugins/hideAttachments/types.ts | 44 ++++++ src/plugins/hideAttachments/utils.ts | 22 +++ src/utils/constants.ts | 4 + 4 files changed, 239 insertions(+), 36 deletions(-) create mode 100644 src/plugins/hideAttachments/types.ts create mode 100644 src/plugins/hideAttachments/utils.ts diff --git a/src/plugins/hideAttachments/index.tsx b/src/plugins/hideAttachments/index.tsx index 39935d038..bc98b5bb4 100644 --- a/src/plugins/hideAttachments/index.tsx +++ b/src/plugins/hideAttachments/index.tsx @@ -17,14 +17,16 @@ */ import { get, set } from "@api/DataStore"; +import { definePluginSettings } from "@api/Settings"; import { ImageInvisible, ImageVisible } from "@components/Icons"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore } from "@webpack/common"; -import { MessageSnapshot } from "@webpack/types"; + +import { ILoadMessagesSuccessPayload, IMessage, IMessageCreatePayload, IMessageUpdatePayload } from "./types"; +import { isStringEmpty } from "./utils"; let style: HTMLStyleElement; - const KEY = "HideAttachments_HiddenIds"; let hiddenMessages: Set = new Set(); @@ -34,18 +36,157 @@ const getHiddenMessages = () => get(KEY).then(set => { }); const saveHiddenMessages = (ids: Set) => set(KEY, ids); +/** + * Update CSS + */ +const buildCss = async () => { + const elements = [...hiddenMessages] + .map(x => `#message-accessories-${x}`) + .join(","); + + style.textContent = ` + :is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) { + /* important is not necessary, but add it to make sure bad themes won't break it */ + display: none !important; + } + :is(${elements})::after { + content: "Attachments hidden"; + color: var(--text-muted); + font-size: 80%; + } + `; +}; + +/** + * Toggle attachment/embed hiding + */ +const toggleHide = async (id: string): Promise => { + const ids = await getHiddenMessages(); + if (!ids.delete(id)) + ids.add(id); + + await saveHiddenMessages(ids); + await buildCss(); +}; + +/** + * Determine if the message should be blocked according to user ID filter + * @param {Message} payload The message to be checked + * @param {string[]} userFilters List of user IDs to be checked + * @returns {boolean} + */ +const shouldHideByUserIdFilter = (payload: IMessage, userFilters: string[]): boolean => { + for (const id of userFilters) { + if (payload.author.id === id) { + return true; + } + } + + return false; +}; + +/** + * Determine if the message should be blocked according to domain list filter + * @param {Message} payload The message to be checked + * @param {string[]} domainList List of domains to be checked + * @returns {boolean} + */ +const shouldHideByDomainListFilter = (payload: IMessage, domainList: string[]): boolean => { + if (payload.embeds.length <= 0) { + return false; + } + + for (const embed of payload.embeds) { + if (!embed.url) { + continue; + } + + for (const domain of domainList) { + const host = URL.parse(embed.url)?.hostname ?? ""; + if (host.indexOf(domain) >= 0) { + return true; + } + } + } + + return false; +}; + +/** + * Checks and hides the attachment/embed + * @param {Message} message The message to check + * @param {object} store The configuration values + */ +const checkAndHide = async (message: IMessage, store: typeof settings.store): Promise => { + if (!store.enableAutoHideAttachments) { + return; + } + + if (hiddenMessages.has(message.id)) { + return; + } + + const userFilters = isStringEmpty(store.filterUserList) + ? [] + : store.filterUserList.split(","); + if (shouldHideByUserIdFilter(message, userFilters)) { + await toggleHide(message.id); + return; + } + + const domainFilters = isStringEmpty(store.filterDomainList) + ? [] + : store.filterDomainList.split(","); + if (shouldHideByDomainListFilter(message, domainFilters)) { + await toggleHide(message.id); + return; + } + + // Forwarded messages + // Limitation: User filters don't work on this one. + if (Array.isArray(message.message_snapshots)) { + for (const snapshot of message.message_snapshots!) { + if (shouldHideByDomainListFilter(snapshot.message, domainFilters)) { + await toggleHide(message.id); + return; + } + } + } +}; + +const settings = definePluginSettings({ + enableAutoHideAttachments: { + type: OptionType.BOOLEAN, + description: "Enable auto hide attachments", + default: false, + restartNeeded: false + }, + filterUserList: { + type: OptionType.STRING, + description: "Comma separated list of User IDs to automatically hide their attachments/embeds", + default: "", + restartNeeded: true + }, + filterDomainList: { + type: OptionType.STRING, + description: "Comma separated list of domains to automatically hide their embeds.", + default: "", + restartNeeded: true + } +}); + export default definePlugin({ name: "HideAttachments", description: "Hide attachments and Embeds for individual messages via hover button", - authors: [Devs.Ven], + authors: [Devs.Ven, Devs.aiko], - renderMessagePopoverButton(msg) { - // @ts-ignore - discord-types lags behind discord. - const hasAttachmentsInShapshots = msg.messageSnapshots.some( - (snapshot: MessageSnapshot) => snapshot?.message.attachments.length - ); + settings, - if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length && !hasAttachmentsInShapshots) return null; + renderMessagePopoverButton(msg: IMessage) { + const hasAttachmentsInSnapshots = !Array.isArray(msg.message_snapshots); + if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length && !hasAttachmentsInSnapshots) { + return null; + } const isHidden = hiddenMessages.has(msg.id); @@ -54,45 +195,37 @@ export default definePlugin({ icon: isHidden ? ImageVisible : ImageInvisible, message: msg, channel: ChannelStore.getChannel(msg.channel_id), - onClick: () => this.toggleHide(msg.id) + onClick: () => toggleHide(msg.id) }; }, + flux: { + async LOAD_MESSAGES_SUCCESS(payload: ILoadMessagesSuccessPayload) { + for (const message of payload.messages) { + await checkAndHide(message, settings.store); + } + }, + + async MESSAGE_CREATE({ message }: IMessageCreatePayload) { + await checkAndHide(message, settings.store); + }, + + async MESSAGE_UPDATE({ message }: IMessageUpdatePayload) { + await checkAndHide(message, settings.store); + } + }, + async start() { style = document.createElement("style"); style.id = "VencordHideAttachments"; document.head.appendChild(style); await getHiddenMessages(); - await this.buildCss(); + await buildCss(); }, stop() { style.remove(); hiddenMessages.clear(); - }, - - async buildCss() { - const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(","); - style.textContent = ` - :is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) { - /* important is not necessary, but add it to make sure bad themes won't break it */ - display: none !important; - } - :is(${elements})::after { - content: "Attachments hidden"; - color: var(--text-muted); - font-size: 80%; - } - `; - }, - - async toggleHide(id: string) { - const ids = await getHiddenMessages(); - if (!ids.delete(id)) - ids.add(id); - - await saveHiddenMessages(ids); - await this.buildCss(); } }); diff --git a/src/plugins/hideAttachments/types.ts b/src/plugins/hideAttachments/types.ts new file mode 100644 index 000000000..e6b25440f --- /dev/null +++ b/src/plugins/hideAttachments/types.ts @@ -0,0 +1,44 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Message } from "discord-types/general"; + +export interface ILoadMessagesSuccessPayload { + channelId: string; + messages: Array; +} + +export interface IMessage extends Message { + message_reference?: { + type: number; + channel_id: string; + message_id: string; + guild_id: string; + }, + message_snapshots?: { + message: Message; + }[] +} + +export interface IMessageCreatePayload { + message: IMessage; +} + +export interface IMessageUpdatePayload { + message: IMessage; +} diff --git a/src/plugins/hideAttachments/utils.ts b/src/plugins/hideAttachments/utils.ts new file mode 100644 index 000000000..2b0fcb6ef --- /dev/null +++ b/src/plugins/hideAttachments/utils.ts @@ -0,0 +1,22 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +export function isStringEmpty (str: string) { + if (!str) return false; + return str.trim().length === 0; +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e75825912..b95b036dd 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -579,6 +579,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "jamesbt365", id: 158567567487795200n, }, + aiko: { + name: "kima_riiiiiii", + id: 366434327761911808n + } } satisfies Record); // iife so #__PURE__ works correctly