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 --- src/components/ErrorBoundary.tsx | 2 +- src/plugins/showHiddenChannels.tsx | 291 --------------------- .../components/HiddenChannelLockScreen.tsx | 202 ++++++++++++++ src/plugins/showHiddenChannels/index.tsx | 274 +++++++++++++++++++ src/plugins/showHiddenChannels/style.css | 78 ++++++ src/utils/index.ts | 1 + src/utils/text.ts | 25 ++ 7 files changed, 581 insertions(+), 292 deletions(-) delete mode 100644 src/plugins/showHiddenChannels.tsx 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') diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 8ebc61b..a13640e 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -103,7 +103,7 @@ const ErrorBoundary = LazyComponent(() => { }; }) as React.ComponentType> & { - wrap(Component: React.ComponentType, errorBoundaryProps?: Props): React.ComponentType; + wrap(Component: React.ComponentType, errorBoundaryProps?: Props): React.ComponentType; }; ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => ( diff --git a/src/plugins/showHiddenChannels.tsx b/src/plugins/showHiddenChannels.tsx deleted file mode 100644 index 283eb83..0000000 --- a/src/plugins/showHiddenChannels.tsx +++ /dev/null @@ -1,291 +0,0 @@ -/* - * 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 { definePluginSettings } from "@api/settings"; -import { Badge } from "@components/Badge"; -import { Flex } from "@components/Flex"; -import { Devs } from "@utils/constants"; -import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { proxyLazy } from "@utils/proxyLazy"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findLazy } from "@webpack"; -import { Button, ChannelStore, moment, Parser, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common"; -import { Channel } from "discord-types/general"; - -const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer"); -const Permissions = findLazy(m => typeof m.VIEW_CHANNEL === "bigint"); -const ChannelTypes = findByPropsLazy("GUILD_TEXT", "GUILD_FORUM"); - -const ChannelTypesToChannelName = proxyLazy(() => ({ - [ChannelTypes.GUILD_TEXT]: "TEXT", - [ChannelTypes.GUILD_ANNOUNCEMENT]: "ANNOUNCEMENT", - [ChannelTypes.GUILD_FORUM]: "FORUM" -})); - -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: "$" - } - ] - }, - { - // inside the onMouseDown handler, we check if the channel is hidden and open the modal if it is - find: "VoiceChannel.renderPopout: There must always be something to render", - replacement: [ - { - match: /(?=(?\i)\.handleThreadsPopoutClose\(\))/, - replace: "if($self.isHiddenChannel($.props.channel)&&arguments[0].button===0){" - + "$self.onHiddenChannelSelected($.props.channel);" - + "return;" - + "}" - }, - // 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($)" - } - }, - // 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))" - } - }, - ], - - 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); - }, - - onHiddenChannelSelected(channel: Channel) { - // Check for type, otherwise it would attempt to show the modal for stage channels - if ([ChannelTypes.GUILD_TEXT, ChannelTypes.GUILD_ANNOUNCEMENT, ChannelTypes.GUILD_FORUM].includes(channel.type)) { - openModal(modalProps => ( - - - - #{channel.name} - {} - {channel.isNSFW() && } - - - - You don't have permission to view {channel.type === ChannelTypes.GUILD_FORUM ? "posts" : "messages"} in this channel. - {(channel.topic ?? "").length > 0 && ( - <> - - {channel.type === ChannelTypes.GUILD_FORUM ? "Guidelines:" : "Topic:"} - -
- {Parser.parseTopic(channel.topic, false, { channelId: channel.id })} -
- - )} - {channel.lastMessageId && ( - <> - - {channel.type === ChannelTypes.GUILD_FORUM ? "Last Post Created" : "Last Message Sent:"} - -
- -
- - )} -
- - - - - -
- )); - } - }, - - LockIcon: () => ( - - - - ), - - HiddenChannelIcon: () => ( - - {({ onMouseLeave, onMouseEnter }) => ( - - - - )} - - ) -}); 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; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 41e1597..b80bde3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -27,4 +27,5 @@ export * as Modals from "./modal"; export * from "./onceDefined"; export * from "./proxyLazy"; export * from "./Queue"; +export * from "./text"; diff --git a/src/utils/text.ts b/src/utils/text.ts index 17826e8..fae3343 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import { moment } from "@webpack/common"; + // Utils for readable text transformations eg: `toTitle(fromKebab())` // Case style to words @@ -34,3 +36,26 @@ export const wordsToPascal = (words: string[]) => words.map(w => w[0].toUpperCase() + w.slice(1)).join(""); export const wordsToTitle = (words: string[]) => words.map(w => w[0].toUpperCase() + w.slice(1)).join(" "); + +/** + * Forms milliseconds into a human readable string link "1 day, 2 hours, 3 minutes and 4 seconds" + * @param ms Milliseconds + * @param short Whether to use short units like "d" instead of "days" + */ +export function formatDuration(ms: number, short: boolean = false) { + const dur = moment.duration(ms); + return (["years", "months", "weeks", "days", "hours", "minutes", "seconds"] as const).reduce((res, unit) => { + const x = dur[unit](); + if (x > 0 || res.length) { + if (res.length) + res += unit === "seconds" ? " and " : ", "; + + const unitStr = short + ? unit[0] + : x === 1 ? unit.slice(0, -1) : unit; + + res += `${x} ${unitStr}`; + } + return res; + }, "").replace(/((,|and) \b0 \w+)+$/, "") || "now"; +} -- cgit