/*
* 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 })
});