ContentWarning: Add OnClick Setting

This commit is contained in:
thororen1234 2025-10-21 21:18:12 -04:00
parent 2b2cb6a17e
commit 3719a7520d
No known key found for this signature in database
6 changed files with 335 additions and 204 deletions

View file

@ -4,8 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { HeadingTertiary } from "@components/Heading";
import { DeleteIcon } from "@components/Icons";
@ -14,6 +17,8 @@ import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { Button, TextInput, useState } from "@webpack/common";
const cl = classNameFactory("vc-content-warning-");
const WORDS_KEY = "ContentWarning_words";
let triggerWords = [""];
@ -29,26 +34,31 @@ function safeMatchesRegex(s: string, r: string) {
function TriggerContainer({ child }) {
const [visible, setVisible] = useState(false);
const { onClick } = settings.store;
const className = onClick ? cl("container") : "";
if (visible) {
return child;
} else {
return (<div onClick={() => setVisible(true)}>
<div style={{
filter: "blur(4px) brightness(70%)",
transition: "filter 0.2s ease-in-out",
cursor: "pointer",
}}
return (
<div
className={className}
onClick={() => onClick && setVisible(true)}
onMouseEnter={event => {
event.currentTarget.style.filter = "none";
if (!onClick) {
event.currentTarget.className = cl("enter");
}
}}
onMouseLeave={event => {
event.currentTarget.style.filter = "blur(4px) brightness(70%)";
if (!onClick) {
event.currentTarget.className = cl("leave");
}
}}
>
{child}
</div>
</div>);
</div >
);
}
}
@ -118,36 +128,43 @@ function FlaggedWords() {
);
});
return (<>
<HeadingTertiary>Flagged Words</HeadingTertiary>
{inputs}
</>);
return (
<>
<HeadingTertiary>Flagged Words</HeadingTertiary>
{inputs}
</>
);
}
const settings = definePluginSettings({
flagged: {
type: OptionType.COMPONENT,
component: () => <FlaggedWords />,
},
onClick: {
type: OptionType.BOOLEAN,
description: "Only show trigger content on click instead of hover",
default: false,
}
});
export default definePlugin({
name: "ContentWarning",
authors: [EquicordDevs.camila314],
description: "Allows you to specify certain trigger words that will be blurred by default. Hovering on the blurred content will reveal it.",
description: "Allows you to specify certain trigger words that will be blurred by default. Hovering/Clicking on the blurred content will reveal it.",
settings,
patches: [
{
find: ".VOICE_HANGOUT_INVITE?",
replacement: {
match: /(compact:\i}=(\i).+?)(\(0,.+\}\)\]\}\))/,
replace: "$1 $self.modify($2,$3)"
replace: "$1 $self.modify(arguments[0].message,$3)"
}
}
],
modify(e, child) {
if (triggerWords.some(word => safeMatchesRegex(e.message.content, word))) {
modify(message, child) {
if (triggerWords.some(w => safeMatchesRegex(message.content, w))) {
return <TriggerContainer child={child} />;
} else {
return child;

View file

@ -0,0 +1,13 @@
.vc-content-warning-container {
filter: blur(5px) brightness(30%);
transition: filter 0.2s ease-in-out;
cursor: pointer;
}
.vc-content-warning-enter {
filter: none;
}
.vc-content-warning-leave {
filter: blur(5px) brightness(30%);
}

View file

@ -9,9 +9,9 @@ import "./styles.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
import { getCurrentChannel } from "@utils/discord";
import definePlugin from "@utils/types";
import { Channel, User } from "@vencord/discord-types";
import { Channel, Guild, User } from "@vencord/discord-types";
import {
DefaultExtractAndLoadChunksRegex,
extractAndLoadChunksLazy,
@ -26,14 +26,19 @@ import {
ChannelRouter,
ChannelStore,
FluxDispatcher,
GuildStore,
Menu,
MessageActions,
MessageStore,
PermissionsBits,
PermissionStore,
PopoutActions,
React,
RelationshipStore,
SelectedChannelStore,
SelectedGuildStore,
useCallback,
useEffect,
useLayoutEffect,
UserStore,
useState,
useStateFromStores
@ -50,61 +55,87 @@ const { HeaderBar, /* HeaderBarIcon*/ } = mapMangledModuleLazy(".themedMobile]:"
// from toolbox
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
const ArrowsLeftRightIcon = () => {
return <svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="var(--interactive-normal)" d="M2.3 7.7a1 1 0 0 1 0-1.4l4-4a1 1 0 0 1 1.4 1.4L5.42 6H21a1 1 0 1 1 0 2H5.41l2.3 2.3a1 1 0 1 1-1.42 1.4l-4-4ZM17.7 21.7l4-4a1 1 0 0 0 0-1.4l-4-4a1 1 0 0 0-1.4 1.4l2.29 2.3H3a1 1 0 1 0 0 2h15.59l-2.3 2.3a1 1 0 0 0 1.42 1.4Z"></path></svg>;
const { ForumView } = mapMangledModuleLazy("forum-grid-header-section-", {
ForumView: filters.byCode("sidebarState")
});
const ArrowsLeftRightIcon = ({ color, ...rest }) => {
return (
<svg
aria-hidden="true"
role="img"
xmlns="http://www.w3.org/2000/svg"
fill={color}
viewBox="0 0 24 24"
{...rest}>
<path d="M2.3 7.7a1 1 0 0 1 0-1.4l4-4a1 1 0 0 1 1.4 1.4L5.42 6H21a1 1 0 1 1 0 2H5.41l2.3 2.3a1 1 0 1 1-1.42 1.4l-4-4ZM17.7 21.7l4-4a1 1 0 0 0 0-1.4l-4-4a1 1 0 0 0-1.4 1.4l2.29 2.3H3a1 1 0 1 0 0 2h15.59l-2.3 2.3a1 1 0 0 0 1.42 1.4Z" />
</svg>
);
};
const WindowLaunchIcon = findComponentByCodeLazy("1-1h6a1 1 0 1 0 0-2H5Z");
const XSmallIcon = findComponentByCodeLazy("1.4L12 13.42l5.3 5.3Z");
const Chat = findComponentByCodeLazy("filterAfterTimestamp:", "chatInputType");
const Resize = findComponentByCodeLazy("sidebarType:", ".RESIZE_HANDLE_WIDTH");
const Resize = findComponentByCodeLazy("sidebarType:", "RESIZE_HANDLE_WIDTH)");
const ChannelHeader = findComponentByCodeLazy(".forumPostTitle]:", '"channel-".concat');
const PopoutWindow = findComponentByCodeLazy("Missing guestWindow reference");
const FullChannelView = findComponentByCodeLazy("showFollowButton:(null");
const WanderingCubesLoading = findComponentByCodeLazy('="wanderingCubes"');
// love
const ppStyle = findLazy(m => m?.popoutContent && Object.keys(m).length === 1);
const ChatInputTypes = findByPropsLazy("FORM", "NORMAL");
const Sidebars = findByPropsLazy("ThreadSidebar", "MessageRequestSidebar");
const ChatClasses = findByPropsLazy("threadSidebarOpen");
const ChannelSectionStore = findStoreLazy("ChannelSectionStore");
const requireChannelContextMenu = extractAndLoadChunksLazy(["&&this.handleActivitiesPopoutClose(),"], new RegExp(DefaultExtractAndLoadChunksRegex.source + ".{1,150}isFavorite"));
const requireChannelContextMenu = extractAndLoadChunksLazy(
["&&this.handleActivitiesPopoutClose(),"],
new RegExp(DefaultExtractAndLoadChunksRegex.source + ".{1,150}isFavorite")
);
const requireForumView = extractAndLoadChunksLazy(
["Missing channel in Channel.renderHeaderToolbar"],
new RegExp(DefaultExtractAndLoadChunksRegex.source + '.{1,150}name:"ForumChannel"')
);
interface ContextMenuProps {
channel: Channel;
guildId?: string;
user: User;
}
const MakeContextMenu = (id: string, guildId: string | null) => {
return (
<Menu.MenuItem
id={`vc-sidebar-chat-${id}`}
label={"Open Sidebar Chat"}
action={() => {
FluxDispatcher.dispatch({
// @ts-ignore
type: "VC_SIDEBAR_CHAT_NEW",
guildId,
id,
});
}}
/>
);
};
function MakeContextCallback(name: "user" | "channel"): NavContextMenuPatchCallback {
return (children, { user, channel }: ContextMenuProps) => {
const isUser = name === "user";
if (isUser && !user) return;
if (!isUser && (!channel || channel.type === 4)) return;
const UserContextPatch: NavContextMenuPatchCallback = (children, args: { user: User; }) => {
const checks = [
args.user,
args.user.id !== UserStore.getCurrentUser().id,
];
if (checks.some(check => !check)) return;
children.push(MakeContextMenu(args.user.id, null));
};
if (isUser && user.id === UserStore.getCurrentUser().id) return;
if (!isUser && (!PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) && channel.type !== 3)) return;
children.push(
<Menu.MenuItem
id={`vc-sidebar-chat-${name}`}
label={"Open Sidebar Chat"}
action={() => {
FluxDispatcher.dispatch({
// @ts-ignore
type: "NEW_SIDEBAR_CHAT",
isUser,
guildId: channel.guild_id,
id: isUser ? user.id : channel.id,
});
}}
/>
);
};
}
const ChannelContextPatch: NavContextMenuPatchCallback = (children, args: { channel: Channel; }) => {
const checks = [
args.channel,
args.channel.type !== 4, // categories
PermissionStore.can(PermissionsBits.VIEW_CHANNEL, args.channel),
];
if (checks.some(check => !check)) return;
children.push(MakeContextMenu(args.channel.id, args.channel.guild_id));
};
export default definePlugin({
name: "SidebarChat",
@ -113,140 +144,204 @@ export default definePlugin({
patches: [
{
find: 'case"pendingFriends":',
replacement: {
match: /return(\(0,\i\.jsxs?\)\(\i\.\i,{}\))}/,
replace: "return [$1, $self.renderSidebar()]}"
}
group: true,
replacement: [
{
match: /channel_renderer"\);/,
replace: "$&const vc_SidebarChat=$self.renderSidebar();"
},
{
match: /return(\(0,\i\.jsxs?\)\(\i\.\i,{}\))}/,
replace: "return [$1, vc_SidebarChat]}"
},
{
match: /(case \i\.\i.+?return)(.+?);(?=.+?params\.messageId)(?<=channel_renderer".+?)/g,
replace: "$1[$2, vc_SidebarChat];",
predicate: () => settings.store.patchCommunity
}
]
},
],
settings,
contextMenus: {
"user-context": MakeContextCallback("user"),
"channel-context": MakeContextCallback("channel"),
"thread-context": MakeContextCallback("channel"),
"gdm-context": MakeContextCallback("channel"),
"user-context": UserContextPatch,
"channel-context": ChannelContextPatch,
"thread-context": ChannelContextPatch,
"gdm-context": ChannelContextPatch,
},
renderSidebar: ErrorBoundary.wrap(() => {
const { guild, channel, /* width*/ } = useStateFromStores([SidebarStore], () => SidebarStore.getFullState());
const [width, setWidth] = useState(0);
toolboxActions: {
"Open Previous Chat"() {
FluxDispatcher.dispatch({
// @ts-ignore
type: "VC_SIDEBAR_CHAT_PREVIOUS",
});
}
},
renderSidebar() {
const { guild, channel /* width*/ } = useStateFromStores(
[SidebarStore, GuildStore, ChannelStore], () => {
const { channelId, guildId } = SidebarStore.getState();
return {
guild: GuildStore.getGuild(guildId),
channel: ChannelStore.getChannel(channelId)
};
}, []
);
const [channelSidebar, guildSidebar] = useStateFromStores(
[ChannelSectionStore],
() => [
ChannelSectionStore.getSidebarState(getCurrentChannel()?.id),
ChannelSectionStore.getGuildSidebarState(getCurrentGuild()?.id),
]
[ChannelSectionStore, SelectedChannelStore, ChannelStore], () => {
const currentChannelId = SelectedChannelStore.getChannelId();
const currentGuildId = SelectedGuildStore.getGuildId();
return [
ChannelSectionStore.getSidebarState(currentChannelId),
ChannelSectionStore.getGuildSidebarState(currentGuildId),
];
}, []
);
useEffect(() => {
if (channel) {
if (MessageStore.getLastMessage(channel.id)) return;
MessageActions.fetchMessages({
channelId: channel.id,
limit: 50,
});
}
}, [channel]);
if (!channel?.id || MessageStore.getLastMessage(channel.id)) return;
MessageActions.fetchMessages({
channelId: channel.id,
limit: 50,
});
}, [channel?.id]);
useEffect(() => {
if (width === 0) setWidth(window.innerWidth);
const [width, setWidth] = useState(window.innerWidth);
useLayoutEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
return () => window.removeEventListener("resize", handleResize);
}, []);
const [View, setViewComponent] = useState<React.ReactNode>(null);
useEffect(() => {
if (!channel) return;
// @ts-expect-error
if (channel.isForumLikeChannel()) {
requireForumView().then(() => {
setViewComponent(
<ForumView
channel={channel}
guild={guild}
sidebarState={null}
/>
);
});
setViewComponent(
<div className={ChatClasses.loader}>
<WanderingCubesLoading />
</div>
);
} else {
setViewComponent(
<Chat
channel={channel}
guild={guild}
chatInputType={ChatInputTypes.SIDEBAR}
/>
);
}
}, [channel]);
if (!channel || channelSidebar || guildSidebar) return null;
return (
<Resize
sidebarType={Sidebars.MessageRequestSidebar}
maxWidth={~~(width * 0.31)/* width - 690*/
}
>
<HeaderBar
toolbar={
<>
<HeaderBarIcon
icon={ArrowsLeftRightIcon}
tooltip="Switch channels"
onClick={() => {
const currentChannel = getCurrentChannel()!;
FluxDispatcher.dispatch({
// @ts-ignore
type: "NEW_SIDEBAR_CHAT",
isUser: currentChannel.id === "1",
guildId: currentChannel?.guild_id,
id: currentChannel.id,
});
ChannelRouter.transitionToChannel(channel.id);
}}
/>
<HeaderBarIcon
icon={WindowLaunchIcon}
tooltip="Popout Chat"
onClick={async () => {
await requireChannelContextMenu();
PopoutActions.open(
`DISCORD_VC_SC-${channel.id}`,
() => <RenderPopout channel={channel} />,
{
defaultWidth: 854,
defaultHeight: 480
});
}}
/>
<HeaderBarIcon
icon={XSmallIcon}
tooltip="Close Sidebar Chat"
onClick={() => {
FluxDispatcher.dispatch({
// @ts-ignore
type: "CLOSE_SIDEBAR_CHAT",
});
}}
/>
</>
}
<ErrorBoundary noop>
<Resize
sidebarType={Sidebars.MessageRequestSidebar}
maxWidth={~~(width * 0.31)/* width - 690*/}
>
<ChannelHeader
channel={channel}
channelName={channel?.name}
guild={guild}
parentChannel={ChannelStore.getChannel(channel?.parent_id)}
/>
</HeaderBar>
<Chat
channel={channel}
guild={guild}
chatInputType={ChatInputTypes.SIDEBAR}
/>
</Resize>
<Header channel={channel} guild={guild} />
{View}
</Resize>
</ErrorBoundary>
);
}),
},
});
const RenderPopout = ErrorBoundary.wrap(({ channel }: { channel: Channel; }) => {
const Header = ({ guild, channel }: { guild: Guild; channel: Channel; }) => {
const recipientId = channel.isPrivate() ? channel.getRecipientId() as string : null;
const name = useStateFromStores([UserStore, RelationshipStore], () => {
if (!recipientId || channel.name) return channel.name;
const user = UserStore.getUser(recipientId);
return RelationshipStore.getNickname(recipientId) || user?.globalName || user?.username;
}, [recipientId, channel.name]);
const parentChannel = useStateFromStores(
[ChannelStore], () => ChannelStore.getChannel(channel?.parent_id),
[channel?.parent_id]
);
// @ts-ignore
const closeSidebar = () => FluxDispatcher.dispatch({ type: "VC_SIDEBAR_CHAT_CLOSE", });
const openPopout = useCallback(async () => {
await requireChannelContextMenu();
PopoutActions.open(
`DISCORD_VC_SC-${channel.id}`,
() => <RenderPopout channel={channel} name={name} />,
{
defaultWidth: 854,
defaultHeight: 480,
}
);
}, [channel, name]);
const switchChannels = useCallback(() => {
const mainChannel = getCurrentChannel()!;
FluxDispatcher.dispatch({
// @ts-ignore
type: "VC_SIDEBAR_CHAT_NEW",
guildId: mainChannel.guild_id,
id: mainChannel.id,
});
ChannelRouter.transitionToChannel(channel.id);
}, [channel.id]);
return (
<HeaderBar
toolbar={
<>
<HeaderBarIcon icon={ArrowsLeftRightIcon} tooltip="Switch channels" onClick={switchChannels} />
<HeaderBarIcon icon={WindowLaunchIcon} tooltip="Popout Chat" onClick={openPopout} />
<HeaderBarIcon icon={XSmallIcon} tooltip="Close Sidebar Chat" onClick={closeSidebar} />
</>
}
>
<ChannelHeader
channel={channel}
channelName={name}
guild={guild}
parentChannel={parentChannel}
/>
</HeaderBar>
);
};
const RenderPopout = ErrorBoundary.wrap(({ channel, name }: { channel: Channel; name: string; }) => {
// Copy from an unexported function of the one they use in the experiment
// right click a channel and search withTitleBar:!0,windowKey
const { Provider } = React.createContext<string | undefined>(undefined);
const selectedChannel = ChannelStore.getChannel(channel.id);
return (
<PopoutWindow
withTitleBar={true}
windowKey={`DISCORD_VC_SC-${selectedChannel.id}`}
title={selectedChannel.name}
channelId={selectedChannel.id}
withTitleBar
windowKey={`DISCORD_VC_SC-${channel.id}`}
title={name || "Vencord"}
channelId={channel.id}
contentClassName={ppStyle.popoutContent}
>
<Provider value={selectedChannel.guild_id}>
<FullChannelView providedChannel={selectedChannel} />
</Provider>
<FullChannelView providedChannel={channel} />
</PopoutWindow>
);
});

View file

@ -7,86 +7,84 @@
import { definePluginSettings } from "@api/Settings";
import { proxyLazy } from "@utils/lazy";
import { OptionType } from "@utils/types";
import { FluxEmitter, FluxStore } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack";
import { ChannelActionCreators, ChannelStore, FluxDispatcher, GuildStore } from "@webpack/common";
import { Flux as TFlux } from "@vencord/discord-types";
import { ChannelActionCreators, Flux as FluxWP, FluxDispatcher } from "@webpack/common";
interface IFlux {
PersistedStore: typeof FluxStore;
Emitter: FluxEmitter;
interface IFlux extends TFlux {
PersistedStore: TFlux["Store"];
}
const Flux: IFlux = findByPropsLazy("connectStores");
export const settings = definePluginSettings({
persistSidebar: {
type: OptionType.BOOLEAN,
description: "Keep the sidebar chat open across Discord restarts",
default: true,
},
patchCommunity: {
type: OptionType.BOOLEAN,
description: "Patch things like the Channel Browser or Members tab that community servers have.",
default: true,
restartNeeded: true,
}
});
interface SidebarData {
isUser: boolean;
guildId: string;
id: string;
}
export const SidebarStore = proxyLazy(() => {
let guildId = "";
let channelId = "";
let width = 0;
class SidebarStore extends Flux.PersistedStore {
const current = {
guildId: "",
channelId: "",
width: 0
};
let previous = { ...current };
class SidebarStore extends (FluxWP as IFlux).PersistedStore {
static persistKey = "SidebarStore";
// @ts-ignore
initialize(previous: { guildId?: string; channelId?: string; width?: number; } | undefined) {
if (!settings.store.persistSidebar || !previous) return;
const { guildId: prevGId, channelId: prevCId, width: prevWidth } = previous;
guildId = prevGId || "";
channelId = prevCId || "";
width = prevWidth || 0;
initialize(previousState: { guildId?: string; channelId?: string; width?: number; } | undefined) {
if (!settings.store.persistSidebar || !previousState) return;
const { guildId, channelId, width } = previousState;
current.guildId = guildId || "";
current.channelId = channelId || "";
current.width = width || 0;
}
getState() {
return {
guildId,
channelId,
width
};
}
getFullState() {
return {
guild: GuildStore.getGuild(guildId),
channel: ChannelStore.getChannel(channelId),
width
};
return current;
}
}
const store = new SidebarStore(FluxDispatcher, {
// @ts-ignore
async NEW_SIDEBAR_CHAT({ isUser, guildId: newGId, id }: SidebarData) {
guildId = newGId || "";
async VC_SIDEBAR_CHAT_NEW({ guildId: newGId, id }: { guildId: string | null; id: string; }) {
previous = { ...current };
if (!isUser) {
channelId = id;
current.guildId = newGId || "";
if (current.guildId) {
current.channelId = id;
store.emitChange();
return;
}
channelId = await ChannelActionCreators.getOrEnsurePrivateChannel(id);
current.channelId = await ChannelActionCreators.getOrEnsurePrivateChannel(id);
store.emitChange();
},
CLOSE_SIDEBAR_CHAT() {
guildId = "";
channelId = "";
VC_SIDEBAR_CHAT_PREVIOUS() {
if (previous.channelId) {
current.guildId = previous.guildId;
current.channelId = previous.channelId;
}
store.emitChange();
},
/* SIDEBAR_CHAT_WIDTH({ newWidth }: { newWidth: number; }) {
width = newWidth;
VC_SIDEBAR_CHAT_CLOSE() {
previous = { ...current };
current.guildId = "";
current.channelId = "";
store.emitChange();
}*/
},
});
return store;

View file

@ -5,3 +5,7 @@
line-clamp: 1;
white-space: unset;
}
[class^="page_"] > * {
min-width: 0;
}

View file

@ -2,6 +2,10 @@
flex: none !important;
}
.voice-user-button-container {
padding-right: 4px;
}
div[class^='list_'][class*='collapsed_'] .voice-user-buttons {
display: none;
}