/* * 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 definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common"; import { Channel } from "discord-types/general"; import HiddenChannelLockScreen, { setChannelBeginHeaderComponent, setEmojiComponent } from "./components/HiddenChannelLockScreen"; const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer"); const VIEW_CHANNEL = 1n << 10n; 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: "this.category.isCollapsed?$.WouldShowIfUncollapsed:$.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.{1,40}return \i)/, replace: "$$$}" }, { match: /(?<=renderLevel:(?\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/, replace: "$" }, { match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(?\i)\..+?(?=,)/, replace: "$.Show" }, { match: /(?<=getRenderLevel=function.+?return ).+?\?(?.+?):\i\.CannotShow(?=})/, replace: "$" } ] }, { 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\()(?=.+?\((?\i)\))/, replace: "!$self.isHiddenChannel($)&&" }, { // Make Discord think we are connected to a voice channel so it shows us inside it match: /(?=\|\|\i\.default\.selectVoiceChannel\((?\i)\.id\))/, replace: "||$self.isHiddenChannel($)" }, { // Make Discord think we are connected to a voice channel so it shows us inside it match: /(?<=\|\|\i\.default\.selectVoiceChannel\((?\i)\.id\);!__OVERLAY__&&\()/, replace: "$self.isHiddenChannel($)||" } ] }, { 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(`(?<=\\i\\.${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\).{1,30}\.GUILD_ANNOUNCEMENT.{1,30}\(0,\i\.\i\))/, replace: "if($self.isHiddenChannel($))return $self.LockIcon;" } }, { find: ".UNREAD_HIGHLIGHT", predicate: () => settings.store.hideUnreads === true, replacement: { // Hide unreads match: /(?<=\i\.connected,\i=)(?=(?\i)\.unread)/, replace: "$self.isHiddenChannel($.channel)?false:" } }, { 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: "$self.isHiddenChannel($.channel)?true:" }, // Add the hidden eye icon if the channel is hidden { match: /(?<=(?\i)=\i\.channel,.+?\(\)\.children.+?:null)/, replace: ",$self.isHiddenChannel($)?$self.HiddenChannelIcon():null" }, // Make voice channels also appear as muted if they are muted { match: /(?<=\i\(\)\.wrapper:\i\(\)\.notInteractive,)(?.+?)(?(?\i)\?\i\.MUTED)/, replace: "$:\"\",$$?\"\"" } ] }, // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden { find: ".UNREAD_HIGHLIGHT", predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, replacement: { match: /(?<=(?\i)=\i\.channel,.+?\.LOCKED:\i)/, replace: "&&!($self.settings.store.hideUnreads===false&&$self.isHiddenChannel($))" } }, { // Hide New unreads box for hidden channels find: '.displayName="ChannelListUnreadsStore"', replacement: { match: /(?<=return null!=(?\i))(?=.{1,130}hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module replace: "&&!$self.isHiddenChannel($)" } }, // 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: "if($self.isHiddenChannel($)){$break;}" }, { match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(?.+?{channel:(?\i)},"notifications"\)\)))/, replace: "if($self.isHiddenChannel($)){$;break;}" }, { match: /(?<=(?\i)\.renderMobileToolbar=function.+?case \i\.\i\.GUILD_FORUM:)/, replace: "if($self.isHiddenChannel($.props.channel))break;" }, { match: /(?<=renderHeaderBar=function.+?hideSearch:(?\i)\.isDirectory\(\))/, replace: "||$self.isHiddenChannel($)" }, { 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: /(?<=if\(null!=(?\i)\).{1,100}"Skipping fetch because channelId is a static route".{1,10}else{)/, replace: "if($self.isHiddenChannel({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))(?=.{1,130}hasRelevantUnread\(\i\))/, replace: "&&!$self.isHiddenChannel($)" } }, { find: '"alt+down"', replacement: { match: /(?<=getState\(\)\.channelId.{1,30}\(0,\i\.\i\)\(\i\))(?=\.map\()/, replace: ".filter(ch=>!$self.isHiddenChannel(ch))" } }, // Export the emoji component used on the lock screen { find: 'jumboable?"jumbo":"default"', replacement: { match: /(?<=(?\i)=function.{1,20}node,\i=\i.isInteracting.+?}}\)},)/, replace: "shcEmojiComponentExport=($self.setEmojiComponent($),void 0)," } }, { find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE", replacement: [ { // Export the channel beggining header match: /(?<=function (?\i)\(.{1,600}computePermissionsForRoles.+?}\)})(?=var)/, replace: "$self.setChannelBeginHeaderComponent($);" }, { // Patch the header to only return allowed users and roles if it's a hidden channel (Like when it's used on the HiddenChannelLockScreen) match: /(?<=MANAGE_ROLES.{1,60}return)(?=\(.+?(?\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(?\i)\.guild_id.+?roleColor.+?]}\)))/, replace: " $self.isHiddenChannel($)?$:" } ] }, { find: ".Messages.SHOW_CHAT", replacement: [ { // Remove the divider and the open chat button for the HiddenChannelLockScreen match: /(?<=function \i\((?\i)\).{1,2000}"more-options-popout"\)\);if\()/, replace: "(!$self.isHiddenChannel($.channel)||$.inCall)&&" }, { // Render our HiddenChannelLockScreen component instead of the main voice channel component match: /(?<=renderContent=function.{1,1700}children:)/, replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?$self.HiddenChannelLockScreen(this.props.channel):" }, { // Disable gradients for the HiddenChannelLockScreen of voice channels match: /(?<=renderContent=function.{1,1600}disableGradients:)/, replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)||" }, { // Disable useless components for the HiddenChannelLockScreen of voice channels match: /(?<=renderContent=function.{1,800}render(?!Header).{0,30}:)(?!void)/g, replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?null:" } ] }, { find: "Guild voice channel without guild id.", replacement: [ { // Render our HiddenChannelLockScreen component instead of the main stage channel component match: /(?<=(?\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1400}children:)(?=.{1,20}}\)}function)/, replace: "$self.isHiddenChannel($)?$self.HiddenChannelLockScreen($):" }, { // Disable useless components for the HiddenChannelLockScreen of stage channels match: /(?<=(?\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1000}render(?!Header).{0,30}:)/g, replace: "$self.isHiddenChannel($)?null:" }, // Prevent Discord from replacing our route if we aren't connected to the stage channel { match: /(?<=if\()(?=!\i&&!\i&&!\i.{1,80}(?\i)\.getGuildId\(\).{1,50}Guild voice channel without guild id\.)/, replace: "!$self.isHiddenChannel($)&&" }, { // Disable gradients for the HiddenChannelLockScreen of stage channels match: /(?<=(?\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}disableGradients:)/, replace: "$self.isHiddenChannel($)||" }, { // Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels match: /(?<=(?\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}style:)/, replace: "$self.isHiddenChannel($)?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: "$self.isHiddenChannel($)?null:($&)" }, { // Remove the open chat button for the HiddenChannelLockScreen match: /(?<=null,)(?=.{1,120}channelId:(?\i)\.id,.+?toggleRequestToSpeakSidebar:\i,iconClassName:\i\(\)\.buttonIcon)/, replace: "!$self.isHiddenChannel($)&&" } ], } ], setEmojiComponent, setChannelBeginHeaderComponent, isHiddenChannel(channel: Channel & { channelId?: string; }) { 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); }, HiddenChannelLockScreen: (channel: any) => , LockIcon: () => ( ), HiddenChannelIcon: ErrorBoundary.wrap(() => ( {({ onMouseLeave, onMouseEnter }) => ( )} ), { noop: true }) });