From 369d179bbf67d34fc4d5f8312d19a106f3552373 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 1 Feb 2023 08:11:05 -0300 Subject: ShowHiddenChannels: New screen for showing hidden channels (#460) Co-authored-by: Ven --- .../components/HiddenChannelLockScreen.tsx | 202 +++++++++++++++ src/plugins/showHiddenChannels/index.tsx | 274 +++++++++++++++++++++ src/plugins/showHiddenChannels/style.css | 78 ++++++ 3 files changed, 554 insertions(+) create mode 100644 src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx create mode 100644 src/plugins/showHiddenChannels/index.tsx create mode 100644 src/plugins/showHiddenChannels/style.css (limited to 'src/plugins/showHiddenChannels') diff --git a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx new file mode 100644 index 0000000..e5c5ee2 --- /dev/null +++ b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx @@ -0,0 +1,202 @@ +/* + * 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 ErrorBoundary from "@components/ErrorBoundary"; +import { LazyComponent } from "@utils/misc"; +import { proxyLazy } from "@utils/proxyLazy"; +import { formatDuration } from "@utils/text"; +import { find, findByCode, findByPropsLazy, findLazy } from "@webpack"; +import { moment, Parser, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common"; +import { Channel } from "discord-types/general"; + +enum SortOrderTypesTyping { + LATEST_ACTIVITY = 0, + CREATION_DATE = 1 +} + +enum ForumLayoutTypesTyping { + DEFAULT = 0, + LIST = 1, + GRID = 2 +} + +interface DefaultReaction { + emojiId: string | null; + emojiName: string | null; +} + +interface Tag { + id: string; + name: string; + emojiId: string | null; + emojiName: string | null; + moderated: boolean; +} + +interface ExtendedChannel extends Channel { + defaultThreadRateLimitPerUser?: number; + defaultSortOrder?: SortOrderTypesTyping | null; + defaultForumLayout?: ForumLayoutTypesTyping; + defaultReactionEmoji?: DefaultReaction | null; + availableTags?: Array; +} + +const ChatClasses = findByPropsLazy("chat", "chatContent"); +const TagClasses = findLazy(m => typeof m.tags === "string" && Object.entries(m).length === 1); // Object exported with a single key called tags +const ChannelTypes = findByPropsLazy("GUILD_TEXT", "GUILD_FORUM"); +const SortOrderTypes = findLazy(m => typeof m.LATEST_ACTIVITY === "number"); +const ForumLayoutTypes = findLazy(m => typeof m.LIST === "number"); +const ChannelFlags = findLazy(m => typeof m.REQUIRE_TAG === "number"); +const TagComponent = LazyComponent(() => find(m => { + if (typeof m !== "function") return false; + + const code = Function.prototype.toString.call(m); + // Get the component which doesn't include increasedActivity logic + return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill"); +})); +const EmojiComponent = LazyComponent(() => findByCode('.jumboable?"jumbo":"default"')); + +const ChannelTypesToChannelNames = proxyLazy(() => ({ + [ChannelTypes.GUILD_TEXT]: "text", + [ChannelTypes.GUILD_ANNOUNCEMENT]: "announcement", + [ChannelTypes.GUILD_FORUM]: "forum" +})); + +const SortOrderTypesToNames = proxyLazy(() => ({ + [SortOrderTypes.LATEST_ACTIVITY]: "Latest activity", + [SortOrderTypes.CREATION_DATE]: "Creation date" +})); + +const ForumLayoutTypesToNames = proxyLazy(() => ({ + [ForumLayoutTypes.DEFAULT]: "Not set", + [ForumLayoutTypes.LIST]: "List view", + [ForumLayoutTypes.GRID]: "Gallery view" +})); + +// Icon from the modal when clicking a message link you don't have access to view +const HiddenChannelLogo = "/assets/433e3ec4319a9d11b0cbe39342614982.svg"; + +function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { + const { + type, + topic, + lastMessageId, + defaultForumLayout, + lastPinTimestamp, + defaultAutoArchiveDuration, + availableTags, + id: channelId, + rateLimitPerUser, + defaultThreadRateLimitPerUser, + defaultSortOrder, + defaultReactionEmoji + } = channel; + + return ( +
+ + +
+ This is a hidden {ChannelTypesToChannelNames[type]} channel. + {channel.isNSFW() && + + {({ onMouseLeave, onMouseEnter }) => ( + + + + )} + + } +
+ + + You can not see the {channel.isForumChannel() ? "posts" : "messages"} of this channel. + {channel.isForumChannel() && topic && topic.length > 0 && "However you may see its guidelines:"} + + + {channel.isForumChannel() && topic && topic.length > 0 && ( +
+ {Parser.parseTopic(topic, false, { channelId })} +
+ )} + + {lastMessageId && + + Last {channel.isForumChannel() ? "post" : "message"} created: + + + } + + {lastPinTimestamp && + Last message pin: + } + {(rateLimitPerUser ?? 0) > 0 && + Slowmode: {formatDuration(rateLimitPerUser! * 1000)} + } + {(defaultThreadRateLimitPerUser ?? 0) > 0 && + + Default thread slowmode: {formatDuration(defaultThreadRateLimitPerUser! * 1000)} + + } + {(defaultAutoArchiveDuration ?? 0) > 0 && + + Default inactivity duration before archiving {channel.isForumChannel() ? "posts" : "threads"}: + {formatDuration(defaultAutoArchiveDuration! * 1000 * 60)} + + } + {defaultForumLayout != null && + Default layout: {ForumLayoutTypesToNames[defaultForumLayout]} + } + {defaultSortOrder != null && + Default sort order: {SortOrderTypesToNames[defaultSortOrder]} + } + {defaultReactionEmoji != null && +
+ Default reaction emoji: + +
+ } + {channel.hasFlag(ChannelFlags.REQUIRE_TAG) && + Posts on this forum require a tag to be set. + } + {availableTags && availableTags.length > 0 && +
+ Available tags: +
+ {availableTags.map(tag => )} +
+
+ } +
+ ); +} + +export default ErrorBoundary.wrap(HiddenChannelLockScreen); diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx new file mode 100644 index 0000000..abb443e --- /dev/null +++ b/src/plugins/showHiddenChannels/index.tsx @@ -0,0 +1,274 @@ +/* + * 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, findLazy } from "@webpack"; +import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common"; +import { Channel } from "discord-types/general"; + +import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen"; + +const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer"); +const Permissions = findLazy(m => typeof m.VIEW_CHANNEL === "bigint"); + +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.renderPopout: There must always be something to render", + replacement: [ + // Do nothing when trying to join a voice channel if the channel is hidden + { + match: /(?<=handleClick=function\(\){)(?=.{1,80}(?\i)\.handleVoiceConnect\(\))/, + replace: "if($self.isHiddenChannel($.props.channel))return;" + }, + // 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\))/, + 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:\(\)=>\i)(?=}.+?(?\i)=function.{1,20}node,\i=\i.isInteracting)/, + replace: ",hc1:()=>$" // Blame Ven length check for the small name :pensive_cry: + } + } + ], + + 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(Permissions.VIEW_CHANNEL, channel); + }, + + HiddenChannelLockScreen: (channel: any) => , + + LockIcon: () => ( + + + + ), + + HiddenChannelIcon: ErrorBoundary.wrap(() => ( + + {({ onMouseLeave, onMouseEnter }) => ( + + + + )} + + ), { noop: true }) +}); diff --git a/src/plugins/showHiddenChannels/style.css b/src/plugins/showHiddenChannels/style.css new file mode 100644 index 0000000..73957ef --- /dev/null +++ b/src/plugins/showHiddenChannels/style.css @@ -0,0 +1,78 @@ +.shc-lock-screen-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +.shc-lock-screen-container > * { + margin: 5px; +} + +.shc-lock-screen-logo { + width: 180px; + height: 180px; +} + +.shc-lock-screen-heading-container { + display: flex; + flex-direction: row; + align-items: center; +} + +.shc-lock-screen-heading-container > * { + margin: inherit; +} + +.shc-lock-screen-heading-nsfw-icon > path { + fill: var(--text-normal); + fill-rule: evenodd; +} + +.shc-lock-screen-topic-container { + color: var(--text-normal); + background-color: var(--background-secondary); + border-radius: 5px; + padding: 5px; + max-width: 70vw; +} + +.shc-lock-screen-tags-container { + background-color: var(--background-secondary); + border-radius: 5px; + padding: 5px; + max-width: 70vw; +} + +.shc-lock-screen-tags-container > * { + margin: inherit; +} + +.shc-lock-screen-tags-container > [class^="tags"] { + flex-wrap: wrap; +} + +.shc-evenodd-fill-current-color { + fill-rule: evenodd; + fill: currentcolor; +} + +.shc-hidden-channel-icon { + margin-left: 6px; + z-index: 0; + cursor: not-allowed; +} + +.shc-lock-screen-default-emoji-container { + display: flex; + flex-direction: row; + align-items: center; +} + +.shc-lock-screen-default-emoji-container > [class^="emojiContainer"] { + background-color: var(--background-secondary); + border-radius: 8px; + padding: 3px 4px; + margin-left: 5px; +} -- cgit