/* * 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 "./style.css"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { canonicalizeMatch } from "@utils/patches"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common"; import type { Channel, Role } from "discord-types/general"; import HiddenChannelLockScreen, { setChannelBeginHeaderComponent } from "./components/HiddenChannelLockScreen"; const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer"); export const VIEW_CHANNEL = 1n << 10n; const CONNECT = 1n << 20n; enum ShowMode { LockIcon, HiddenIconWithMutedStyle } const settings = definePluginSettings({ hideUnreads: { description: "Hide Unreads", type: OptionType.BOOLEAN, default: true, restartNeeded: true }, showMode: { description: "The mode used to display hidden channels.", type: OptionType.SELECT, options: [ { label: "Plain style with Lock Icon instead", value: ShowMode.LockIcon, default: true }, { label: "Muted style with hidden eye icon on the right", value: ShowMode.HiddenIconWithMutedStyle }, ], restartNeeded: true } }); export default definePlugin({ name: "ShowHiddenChannels", description: "Show channels that you do not have access to view.", authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.dzshn], settings, patches: [ { // RenderLevel defines if a channel is hidden, collapsed in category, visible, etc find: ".CannotShow=", // These replacements only change the necessary CannotShow's replacement: [ { match: /(?<=isChannelGatedAndVisible\(this\.record\.guild_id,this\.record\.id\).+?renderLevel:)(\i)\..+?(?=,)/, replace: (_, RenderLevels) => `this.category.isCollapsed?${RenderLevels}.WouldShowIfUncollapsed:${RenderLevels}.Show` }, // Move isChannelGatedAndVisible renderLevel logic to the bottom to not show hidden channels in case they are muted { match: /(?<=(if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{)if\(this\.id===\i\).+?};)(if\(!\i\.\i\.isChannelGatedAndVisible\(.+?})(.+?)(?=return{renderLevel:\i\.Show.{0,40}?return \i)/, replace: (_, permissionCheck, isChannelGatedAndVisibleCondition, rest) => `${rest}${permissionCheck}${isChannelGatedAndVisibleCondition}}` }, { match: /(?<=renderLevel:(\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/, replace: (_, renderLevelExpression) => renderLevelExpression }, { match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(\i)\..+?(?=,)/, replace: (_, RenderLevels) => `${RenderLevels}.Show` }, { match: /(?<=getRenderLevel=function.+?return ).+?\?(.+?):\i\.CannotShow(?=})/, replace: (_, renderLevelExpressionWithoutPermCheck) => renderLevelExpressionWithoutPermCheck } ] }, { find: "VoiceChannel, transitionTo: Channel does not have a guildId", replacement: [ { // Do not show confirmation to join a voice channel when already connected to another if clicking on a hidden voice channel match: /(?<=getCurrentClientVoiceChannelId\((\i)\.guild_id\);if\()/, replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&` }, { // Prevent Discord from trying to connect to hidden channels match: /(?=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\))/, replace: (_, channel) => `||$self.isHiddenChannel(${channel})` }, { // Make Discord show inside the channel if clicking on a hidden or locked channel match: /(?<=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\);!__OVERLAY__&&\()/, replace: (_, channel) => `$self.isHiddenChannel(${channel},true)||` } ] }, { find: "VoiceChannel.renderPopout: There must always be something to render", replacement: [ // Render null instead of the buttons if the channel is hidden ...[ "renderEditButton", "renderInviteButton", "renderOpenChatButton" ].map(func => ({ match: new RegExp(`(?<=${func}=function\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions replace: "if($self.isHiddenChannel(this.props.channel))return null;" })) ] }, { find: ".Messages.CHANNEL_TOOLTIP_DIRECTORY", predicate: () => settings.store.showMode === ShowMode.LockIcon, replacement: { // Lock Icon match: /(?=switch\((\i)\.type\).{0,30}\.GUILD_ANNOUNCEMENT.{0,30}\(0,\i\.\i\))/, replace: (_, channel) => `if($self.isHiddenChannel(${channel}))return $self.LockIcon;` } }, { find: ".UNREAD_HIGHLIGHT", predicate: () => settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, replacement: [ // Make the channel appear as muted if it's hidden { match: /(?<=\i\.name,\i=)(?=(\i)\.muted)/, replace: (_, props) => `$self.isHiddenChannel(${props}.channel)?true:` }, // Add the hidden eye icon if the channel is hidden { match: /\(\).children.+?:null(?<=(\i)=\i\.channel,.+?)/, replace: (m, channel) => `${m},$self.isHiddenChannel(${channel})?$self.HiddenChannelIcon():null` }, // Make voice channels also appear as muted if they are muted { match: /(?<=\.wrapper:\i\(\)\.notInteractive,)(.+?)((\i)\?\i\.MUTED)/, replace: (_, otherClasses, mutedClassExpression, isMuted) => `${mutedClassExpression}:"",${otherClasses}${isMuted}?""` } ] }, { find: ".UNREAD_HIGHLIGHT", replacement: [ { // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, match: /\.LOCKED:\i(?<=(\i)=\i\.channel,.+?)/, replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})` }, { // Hide unreads predicate: () => settings.store.hideUnreads === true, match: /(?<=\i\.connected,\i=)(?=(\i)\.unread)/, replace: (_, props) => `$self.isHiddenChannel(${props}.channel)?false:` } ] }, { // Hide New unreads box for hidden channels find: '.displayName="ChannelListUnreadsStore"', replacement: { match: /(?<=return null!=(\i))(?=.{0,130}?hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` } }, // Only render the channel header and buttons that work when transitioning to a hidden channel { find: "Missing channel in Channel.renderHeaderToolbar", replacement: [ { match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_TEXT:)(?=.+?;(.+?{channel:(\i)},"notifications"\)\);))/, replace: (_, pushNotificationButtonExpression, channel) => `if($self.isHiddenChannel(${channel})){${pushNotificationButtonExpression}break;}` }, { match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(.+?{channel:(\i)},"notifications"\)\)))/, replace: (_, pushNotificationButtonExpression, channel) => `if($self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` }, { match: /renderMobileToolbar=function.+?case \i\.\i\.GUILD_FORUM:(?<=(\i)\.renderMobileToolbar.+?)/, replace: (m, that) => `${m}if($self.isHiddenChannel(${that}.props.channel))break;` }, { match: /(?<=renderHeaderBar=function.+?hideSearch:(\i)\.isDirectory\(\))/, replace: (_, channel) => `||$self.isHiddenChannel(${channel})` }, { match: /(?<=renderSidebar=function\(\){)/, replace: "if($self.isHiddenChannel(this.props.channel))return null;" }, { match: /(?<=renderChat=function\(\){)/, replace: "if($self.isHiddenChannel(this.props.channel))return $self.HiddenChannelLockScreen(this.props.channel);" } ] }, // Avoid trying to fetch messages from hidden channels { find: '"MessageManager"', replacement: { match: /"Skipping fetch because channelId is a static route"\);else{(?=.+?getChannel\((\i)\))/, replace: (m, channelId) => `${m}if($self.isHiddenChannel({channelId:${channelId}}))return;` } }, // Patch keybind handlers so you can't accidentally jump to hidden channels { find: '"alt+shift+down"', replacement: { match: /(?<=getChannel\(\i\);return null!=(\i))(?=.{0,130}?hasRelevantUnread\(\i\))/, replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` } }, { find: '"alt+down"', replacement: { match: /(?<=getState\(\)\.channelId.{0,30}?\(0,\i\.\i\)\(\i\))(?=\.map\()/, replace: ".filter(ch=>!$self.isHiddenChannel(ch))" } }, { find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE", replacement: [ { // Export the channel beginning header match: /computePermissionsForRoles.+?}\)}(?<=function (\i)\(.+?)(?=var)/, replace: (m, component) => `${m}$self.setChannelBeginHeaderComponent(${component});` }, { // Change the role permission check to CONNECT if the channel is locked match: /ADMINISTRATOR\)\|\|(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/, replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):` }, { // Change the permissionOverwrite check to CONNECT if the channel is locked match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/, replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):` }, { // Include the @everyone role in the allowed roles list for Hidden Channels match: /sortBy.{0,100}?return (?<=var (\i)=\i\.channel.+?)(?=\i\.id)/, replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?true:` }, { // If the @everyone role has the required permissions, make the array only contain it match: /computePermissionsForRoles.+?.value\(\)(?<=var (\i)=\i\.channel.+?)/, replace: (m, channel) => `${m}.reduce(...$self.makeAllowedRolesReduce(${channel}.guild_id))` }, { // Patch the header to only return allowed users and roles if it's a hidden channel or locked channel (Like when it's used on the HiddenChannelLockScreen) match: /MANAGE_ROLES.{0,60}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?]}\)))/, replace: (m, component, channel) => { // Export the channel for the users allowed component patch component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,channel:${channel}`); // Always render the component for multiple allowed users component = component.replace(canonicalizeMatch(/1!==\i\.length/), "true"); return `${m} $self.isHiddenChannel(${channel},true)?${component}:`; } } ] }, { find: "().avatars),children", replacement: [ { // Create a variable for the channel prop match: /=(\i)\.maxUsers,/, replace: (m, props) => `${m}channel=${props}.channel,` }, { // Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen match: /\i>0(?=&&.{0,60}renderPopout)/, replace: m => `($self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)?true:${m})` }, { // Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/, replace: (_, amount) => `($self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)&&${amount}<=0?0:1)` }, { // Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen match: /(?<="\+",)(\i)\+1/, replace: (m, amount) => `$self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)&&${amount}<=0?"":${m}` } ] }, { find: ".Messages.SHOW_CHAT", replacement: [ { // Remove the divider and the open chat button for the HiddenChannelLockScreen match: /"more-options-popout"\)\);if\((?<=function \i\((\i)\).+?)/, replace: (m, props) => `${m}!${props}.inCall&&$self.isHiddenChannel(${props}.channel,true)){}else if(` }, { // Remove invite users button for the HiddenChannelLockScreen match: /"popup".{0,100}?if\((?<=(\i)\.channel.+?)/, replace: (m, props) => `${m}(${props}.inCall||!$self.isHiddenChannel(${props}.channel,true))&&` }, { // Render our HiddenChannelLockScreen component instead of the main voice channel component match: /this\.renderVoiceChannelEffects.+?children:(?<=renderContent=function.+?)/, replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?$self.HiddenChannelLockScreen(this.props.channel):" }, { // Disable gradients for the HiddenChannelLockScreen of voice channels match: /this\.renderVoiceChannelEffects.+?disableGradients:(?<=renderContent=function.+?)/, replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)||" }, { // Disable useless components for the HiddenChannelLockScreen of voice channels match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:(?<=renderContent=function.+?)(?!void)/g, replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?null:" }, { // Disable bad CSS class which mess up hidden voice channels styling match: /callContainer,(?<=\(\)\.callContainer,)/, replace: '$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?"":' } ] }, { find: "Guild voice channel without guild id.", replacement: [ { // Render our HiddenChannelLockScreen component instead of the main stage channel component match: /Guild voice channel without guild id.+?children:(?<=(\i)\.getGuildId\(\).+?)(?=.{0,20}?}\)}function)/, replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?$self.HiddenChannelLockScreen(${channel}):` }, { // Disable useless components for the HiddenChannelLockScreen of stage channels match: /render(?!Header).{0,30}?:(?<=(\i)\.getGuildId\(\).+?Guild voice channel without guild id.+?)/g, replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?null:` }, // Prevent Discord from replacing our route if we aren't connected to the stage channel { match: /(?=!\i&&!\i&&!\i.{0,80}?(\i)\.getGuildId\(\).{0,50}?Guild voice channel without guild id)(?<=if\()/, replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&` }, { // Disable gradients for the HiddenChannelLockScreen of stage channels match: /Guild voice channel without guild id.+?disableGradients:(?<=(\i)\.getGuildId\(\).+?)/, replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})||` }, { // Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels match: /Guild voice channel without guild id.+?style:(?<=(\i)\.getGuildId\(\).+?)/, replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?undefined:` }, { // Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen match: /\(0,\i\.jsx\)\(\i\.\i\.Divider.+?}\)]}\)(?=.+?:(\i)\.guild_id)/, replace: (m, channel) => `$self.isHiddenChannel(${channel})?null:(${m})` }, { // Remove the open chat button for the HiddenChannelLockScreen match: /"recents".+?null,(?=.{0,120}?channelId:(\i)\.id)/, replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&` } ], }, { find: "\"^/guild-stages/(\\\\d+)(?:/)?(\\\\d+)?\"", replacement: { // Make mentions of hidden channels work match: /\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,\i\)/, replace: "true" }, }, { find: ".shouldCloseDefaultModals", replacement: { // Show inside voice channel instead of trying to join them when clicking on a channel mention match: /(?<=getChannel\((\i)\)\)(?=.{0,100}?selectVoiceChannel))/, replace: (_, channelId) => `&&!$self.isHiddenChannel({channelId:${channelId}})` } }, { find: '.displayName="GuildChannelStore"', replacement: [ { // Make GuildChannelStore contain hidden channels match: /isChannelGated\(.+?\)(?=\|\|)/, replace: m => `${m}||true` }, { // Filter hidden channels from GuildChannelStore.getChannels unless told otherwise match: /(?<=getChannels=function\(\i)\).+?(?=return (\i)})/, replace: (rest, channels) => `,shouldIncludeHidden=false${rest}${channels}=$self.resolveGuildChannels(${channels},shouldIncludeHidden);` } ] }, { find: ".Messages.FORM_LABEL_MUTED", replacement: { // Make GuildChannelStore.getChannels return hidden channels match: /(?<=getChannels\(\i)(?=\))/, replace: ",true" } } ], setChannelBeginHeaderComponent, isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) { if (!channel) return false; if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId); if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false; return !PermissionStore.can(VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(CONNECT, channel); }, resolveGuildChannels(channels: Record | string | number>, shouldIncludeHidden: boolean) { if (shouldIncludeHidden) return channels; const res = {}; for (const [key, maybeObjChannels] of Object.entries(channels)) { if (!Array.isArray(maybeObjChannels)) { res[key] = maybeObjChannels; continue; } res[key] ??= []; for (const objChannel of maybeObjChannels) { if (objChannel.channel.id === null || !this.isHiddenChannel(objChannel.channel)) res[key].push(objChannel); } } return res; }, makeAllowedRolesReduce(guildId: string) { return [ (prev: Array, _: Role, index: number, originalArray: Array) => { if (index !== 0) return prev; const everyoneRole = originalArray.find(role => role.id === guildId); if (everyoneRole) return [everyoneRole]; return originalArray; }, [] as Array ]; }, HiddenChannelLockScreen: (channel: any) => , LockIcon: () => ( ), HiddenChannelIcon: ErrorBoundary.wrap(() => ( {({ onMouseLeave, onMouseEnter }) => ( )} ), { noop: true }) });