Merge branch 'Vendicated:main' into main

This commit is contained in:
Xyloflake 2025-01-25 00:29:07 +05:30 committed by GitHub
commit 11baed1e4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 151 additions and 77 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.10.9", "version": "1.11.2",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {

View file

@ -79,7 +79,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??" replace: "...$1.props,$& $1.image??"
}, },
{ {
match: /(?<=text:(\i)\.description,.{0,200})children:/, match: /(?<="aria-label":(\i)\.description,.{0,200})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :" replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
}, },
// conditionally override their onClick with badge.onClick if it exists // conditionally override their onClick with badge.onClick if it exists

View file

@ -31,7 +31,7 @@ export default definePlugin({
replace: (match, args) => "" + replace: (match, args) => "" +
`async ${match}` + `async ${match}` +
`if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` + `if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` +
"return Promise.resolve({shoudClear:true,shouldRefocus:true});" "return Promise.resolve({shouldClear:false,shouldRefocus:true});"
} }
}, },
{ {
@ -39,12 +39,12 @@ export default definePlugin({
replacement: { replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); // props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid) // Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
match: /(type:this\.props\.chatInputType.+?\.then\()(\i=>\{.+?let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptionsForReply\(\i\);)(?<=\)\(({.+?})\)\.then.+?)/, match: /(\{openWarningPopout:.{0,100}type:this.props.chatInputType.+?\.then\()(\i=>\{.+?let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptions\(\{.+?\}\);)(?<=\)\(({.+?})\)\.then.+?)/,
// props.chatInputType...then((async function(isMessageValid)... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); if(await Vencord.api...) return { shoudClear:true, shouldRefocus:true }; // props.chatInputType...then((async function(isMessageValid)... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); if(await Vencord.api...) return { shoudClear:true, shouldRefocus:true };
replace: (_, rest1, rest2, parsedMessage, channel, replyOptions, extra) => "" + replace: (_, rest1, rest2, parsedMessage, channel, replyOptions, extra) => "" +
`${rest1}async ${rest2}` + `${rest1}async ${rest2}` +
`if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` + `if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` +
"return{shoudClear:true,shouldRefocus:true};" "return{shouldClear:false,shouldRefocus:true};"
} }
}, },
{ {

View file

@ -16,7 +16,7 @@ import { User } from "discord-types/general";
interface UserProfileProps { interface UserProfileProps {
popoutProps: Record<string, any>; popoutProps: Record<string, any>;
currentUser: User; currentUser: User;
originalPopout: () => React.ReactNode; originalRenderPopout: () => React.ReactNode;
} }
const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined"); const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined");
@ -73,12 +73,12 @@ export default definePlugin({
group: true, group: true,
replacement: [ replacement: [
{ {
match: /(?<=\.SIZE_32\)}\);)/, match: /(?<=\.AVATAR_SIZE\);)/,
replace: "$self.useAccountPanelRef();" replace: "$self.useAccountPanelRef();"
}, },
{ {
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/, match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalPopout:()=>{${originalPopout}}})` replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})`
}, },
{ {
match: /\.AVATAR,children:.+?(?=renderPopout:)/, match: /\.AVATAR,children:.+?(?=renderPopout:)/,
@ -112,17 +112,17 @@ export default definePlugin({
openAlternatePopout = false; openAlternatePopout = false;
}, },
UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalPopout }: UserProfileProps) => { UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalRenderPopout }: UserProfileProps) => {
if ( if (
(settings.store.prioritizeServerProfile && openAlternatePopout) || (settings.store.prioritizeServerProfile && openAlternatePopout) ||
(!settings.store.prioritizeServerProfile && !openAlternatePopout) (!settings.store.prioritizeServerProfile && !openAlternatePopout)
) { ) {
return originalPopout(); return originalRenderPopout();
} }
const currentChannel = getCurrentChannel(); const currentChannel = getCurrentChannel();
if (currentChannel?.getGuildId() == null) { if (currentChannel?.getGuildId() == null) {
return originalPopout(); return originalRenderPopout();
} }
return ( return (

View file

@ -41,7 +41,7 @@ export default definePlugin({
}, },
{ {
// Status emojis // Status emojis
find: "#{intl::GUILD_OWNER}", find: "#{intl::GUILD_OWNER}),children:",
replacement: { replacement: {
match: /(?<=\.activityEmoji,.+?animate:)\i/, match: /(?<=\.activityEmoji,.+?animate:)\i/,
replace: "!0" replace: "!0"

View file

@ -27,7 +27,7 @@ export default definePlugin({
{ {
find: '"ChannelAttachButton"', find: '"ChannelAttachButton"',
replacement: { replacement: {
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/, match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,", replace: "$&onClick:$1,onContextMenu:$2.onClick,",
}, },
}, },

View file

@ -179,6 +179,16 @@ export default definePlugin({
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.", description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
authors: [Devs.Ven], authors: [Devs.Ven],
patches: [
{
find: 'this,"_changeCallbacks",',
replacement: {
match: /\i\(this,"_changeCallbacks",/,
replace: "Reflect.defineProperty(this,Symbol.toStringTag,{value:this.getName(),configurable:!0,writable:!0,enumerable:!1}),$&"
}
}
],
startAt: StartAt.Init, startAt: StartAt.Init,
start() { start() {
const shortcuts = makeShortcuts(); const shortcuts = makeShortcuts();

View file

@ -19,6 +19,7 @@
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; import { getUserSettingLazy } from "@api/UserSettings";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards"; import { isTruthy } from "@utils/guards";
@ -27,15 +28,14 @@ import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack"; import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color"); const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile"); const ActivityView = findComponentByCodeLazy(".party?(0", ".card");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
async function getApplicationAsset(key: string): Promise<string> { async function getApplicationAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0]; return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
} }
@ -169,7 +169,7 @@ const settings = definePluginSettings({
value: TimestampMode.NOW value: TimestampMode.NOW
}, },
{ {
label: "Same as your current time", label: "Same as your current time (not reset after 24h)",
value: TimestampMode.TIME value: TimestampMode.TIME
}, },
{ {
@ -269,6 +269,7 @@ function isStreamLinkDisabled() {
function isStreamLinkValid(value: string) { function isStreamLinkValid(value: string) {
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL."; if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
if (value && value.length > 512) return "Streaming link must be not longer than 512 characters.";
return true; return true;
} }
@ -277,8 +278,9 @@ function isTimestampDisabled() {
} }
function isImageKeyValid(value: string) { function isImageKeyValid(value: string) {
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)"; if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//.test(value)) return "Don't use a Discord link. Use an Imgur image link instead.";
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)"; if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image (e.g. https://i.imgur.com/...). Right click the image and click 'Copy image address'";
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image (e.g. https://media.tenor.com/...). Right click the GIF and click 'Copy image address'";
return true; return true;
} }
@ -390,13 +392,24 @@ async function setRpc(disable?: boolean) {
export default definePlugin({ export default definePlugin({
name: "CustomRPC", name: "CustomRPC",
description: "Allows you to set a custom rich presence.", description: "Add a fully customisable Rich Presence (Game status) to your Discord profile",
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev], authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
dependencies: ["UserSettingsAPI"], dependencies: ["UserSettingsAPI"],
start: setRpc, start: setRpc,
stop: () => setRpc(true), stop: () => setRpc(true),
settings, settings,
patches: [
{
find: ".party?(0",
all: true,
replacement: {
match: /\i\.id===\i\.id\?null:/,
replace: ""
}
}
],
settingsAboutComponent: () => { settingsAboutComponent: () => {
const activity = useAwaiter(createActivity); const activity = useAwaiter(createActivity);
const gameActivityEnabled = ShowCurrentGame.useSetting(); const gameActivityEnabled = ShowCurrentGame.useSetting();
@ -410,7 +423,7 @@ export default definePlugin({
style={{ padding: "1em" }} style={{ padding: "1em" }}
> >
<Forms.FormTitle>Notice</Forms.FormTitle> <Forms.FormTitle>Notice</Forms.FormTitle>
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText> <Forms.FormText>Activity Sharing isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
<Button <Button
color={Button.Colors.TRANSPARENT} color={Button.Colors.TRANSPARENT}
@ -422,24 +435,33 @@ export default definePlugin({
</ErrorCard> </ErrorCard>
)} )}
<Flex flexDirection="column" style={{ gap: ".5em" }} className={Margins.top16}>
<Forms.FormText> <Forms.FormText>
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and Go to the <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID. get the application ID.
</Forms.FormText> </Forms.FormText>
<Forms.FormText> <Forms.FormText>
Upload images in the Rich Presence tab to get the image keys. Upload images in the Rich Presence tab to get the image keys.
</Forms.FormText> </Forms.FormText>
<Forms.FormText> <Forms.FormText>
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address". If you want to use an image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and selecting "Copy image address".
</Forms.FormText> </Forms.FormText>
<Forms.FormText>
You can't see your own buttons on your profile, but everyone else can see it fine.
</Forms.FormText>
<Forms.FormText>
Some weird unicode text ("fonts" 𝖑𝖎𝖐𝖊 𝖙𝖍𝖎𝖘) may cause the rich presence to not show up, try using normal letters instead.
</Forms.FormText>
</Flex>
<Forms.FormDivider className={Margins.top8} /> <Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle, padding: 8, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}> <div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
{activity[0] && <ActivityComponent activity={activity[0]} channelId={SelectedChannelStore.getChannelId()} {activity[0] && <ActivityView
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())} activity={activity[0]}
application={{ id: settings.store.appID }} user={UserStore.getCurrentUser()}
user={UserStore.getCurrentUser()} />} currentUser={UserStore.getCurrentUser()}
/>}
</div> </div>
</> </>
); );

View file

@ -391,7 +391,7 @@ export default definePlugin({
}, },
// Separate patch for allowing using custom app icons // Separate patch for allowing using custom app icons
{ {
find: /\.getCurrentDesktopIcon.{0,25}\.isPremium/, find: "?24:30,",
replacement: { replacement: {
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/, match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true" replace: "true"

View file

@ -13,7 +13,7 @@ export default definePlugin({
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
patches: [ patches: [
{ {
find: "getFormatQuality(){", find: ".handleImageLoad)",
replacement: { replacement: {
match: /(?<=null;return )\i\.\i&&\(\i\|\|!\i\.isAnimated.+?:(?=\i&&\(\i="png"\))/, match: /(?<=null;return )\i\.\i&&\(\i\|\|!\i\.isAnimated.+?:(?=\i&&\(\i="png"\))/,
replace: "" replace: ""

View file

@ -27,7 +27,7 @@ export default definePlugin({
authors: [Devs.D3SOX, Devs.Nickyux], authors: [Devs.D3SOX, Devs.Nickyux],
patches: [ patches: [
{ {
find: "#{intl::GUILD_OWNER}", find: "#{intl::GUILD_OWNER}),children:",
replacement: { replacement: {
match: /,isOwner:(\i),/, match: /,isOwner:(\i),/,
replace: ",_isOwner:$1=$self.isGuildOwner(e)," replace: ",_isOwner:$1=$self.isGuildOwner(e),"

View file

@ -16,7 +16,7 @@ interface UserMentionComponentProps {
id: string; id: string;
channelId: string; channelId: string;
guildId: string; guildId: string;
OriginalComponent: ReactNode; originalComponent: () => ReactNode;
} }
export default definePlugin({ export default definePlugin({
@ -29,7 +29,7 @@ export default definePlugin({
find: ':"text":', find: ':"text":',
replacement: { replacement: {
match: /(hidePersonalInformation\).+?)(if\(null!=\i\){.+?return \i)(?=})/, match: /(hidePersonalInformation\).+?)(if\(null!=\i\){.+?return \i)(?=})/,
replace: "$1return $self.UserMentionComponent({...arguments[0],OriginalComponent:(()=>{$2})()});" replace: "$1return $self.UserMentionComponent({...arguments[0],originalComponent:()=>{$2}});"
} }
} }
], ],
@ -42,6 +42,6 @@ export default definePlugin({
channelId={props.channelId} channelId={props.channelId}
/> />
), { ), {
fallback: ({ wrappedProps }) => wrappedProps.OriginalComponent fallback: ({ wrappedProps: { originalComponent } }) => originalComponent()
}) })
}); });

View file

@ -218,7 +218,7 @@ export default definePlugin({
}, },
// in the member list // in the member list
{ {
find: "#{intl::GUILD_OWNER}", find: "#{intl::GUILD_OWNER}),children:",
replacement: { replacement: {
match: /(?<type>\i)=\(null==.{0,100}\.BOT;return null!=(?<user>\i)&&\i\.bot/, match: /(?<type>\i)=\(null==.{0,100}\.BOT;return null!=(?<user>\i)&&\i\.bot/,
replace: "$<type> = $self.getTag({user: $<user>, channel: arguments[0].channel, origType: $<user>.bot ? 0 : null, location: 'not-chat' }); return typeof $<type> === 'number'" replace: "$<type> = $self.getTag({user: $<user>, channel: arguments[0].channel, origType: $<user>.bot ? 0 : null, location: 'not-chat' }); return typeof $<type> === 'number'"

View file

@ -55,7 +55,7 @@ export default definePlugin({
}, },
{ {
// Clicking on replied messages to jump // Clicking on replied messages to jump
find: "flash:!0,returnMessageId", find: '("interactionUsernameProfile',
replacement: [ replacement: [
{ {
match: /.\?(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/, match: /.\?(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/,

View file

@ -9,16 +9,11 @@ import "./style.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Timestamp } from "@webpack/common"; import { DateUtils, Timestamp } from "@webpack/common";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import type { HTMLAttributes } from "react"; import type { HTMLAttributes } from "react";
const { calendarFormat, dateFormat, isSameDay } = mapMangledModuleLazy("millisecondsInUnit:", {
calendarFormat: filters.byCode("sameElse"),
dateFormat: filters.byCode('":'),
isSameDay: filters.byCode("Math.abs(+"),
});
const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp"); const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp");
function Sep(props: HTMLAttributes<HTMLElement>) { function Sep(props: HTMLAttributes<HTMLElement>) {
@ -46,14 +41,14 @@ function ReplyTimestamp({
return ( return (
<Timestamp <Timestamp
className="vc-reply-timestamp" className="vc-reply-timestamp"
compact={isSameDay(refTimestamp, baseTimestamp)} compact={DateUtils.isSameDay(refTimestamp, baseTimestamp)}
timestamp={refTimestamp} timestamp={refTimestamp}
isInline={false} isInline={false}
> >
<Sep>[</Sep> <Sep>[</Sep>
{isSameDay(refTimestamp, baseTimestamp) {DateUtils.isSameDay(refTimestamp, baseTimestamp)
? dateFormat(refTimestamp, "LT") ? DateUtils.dateFormat(refTimestamp, "LT")
: calendarFormat(refTimestamp) : DateUtils.calendarFormat(refTimestamp)
} }
<Sep>]</Sep> <Sep>]</Sep>
</Timestamp> </Timestamp>

View file

@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
const SpoilerClasses = findByPropsLazy("spoilerContent"); const SpoilerClasses = findByPropsLazy("spoilerContent");
const MessagesClasses = findByPropsLazy("messagesWrapper"); const MessagesClasses = findByPropsLazy("messagesWrapper", "navigationDescription");
export default definePlugin({ export default definePlugin({
name: "RevealAllSpoilers", name: "RevealAllSpoilers",

View file

@ -108,7 +108,7 @@ export default definePlugin({
patches: [ patches: [
{ {
find: "#{intl::MESSAGE_ACTIONS_MENU_LABEL}", find: "#{intl::MESSAGE_ACTIONS_MENU_LABEL}),shouldHideMediaOptions:",
replacement: { replacement: {
match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/, match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/,
replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),` replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),`

View file

@ -58,7 +58,7 @@ export default definePlugin({
}, },
}, },
{ {
find: /context:\i,checkElevated:!1\}\),\i\.\i.{0,200}autoTrackExposure/, find: /,checkElevated:!1}\),\i\.\i\)}(?<=getCurrentUser\(\);return.+?)/,
predicate: () => settings.store.showModView, predicate: () => settings.store.showModView,
replacement: { replacement: {
match: /return \i\.\i\(\i\.\i\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.\i\)/, match: /return \i\.\i\(\i\.\i\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.\i\)/,
@ -67,7 +67,7 @@ export default definePlugin({
}, },
// fixes a bug where Members page must be loaded to see highest role, why is Discord depending on MemberSafetyStore.getEnhancedMember for something that can be obtained here? // fixes a bug where Members page must be loaded to see highest role, why is Discord depending on MemberSafetyStore.getEnhancedMember for something that can be obtained here?
{ {
find: "#{intl::GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL}", find: "#{intl::GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL}),allowOverflow:",
predicate: () => settings.store.showModView, predicate: () => settings.store.showModView,
replacement: { replacement: {
match: /(role:)\i(?=,guildId.{0,100}role:(\i\[))/, match: /(role:)\i(?=,guildId.{0,100}role:(\i\[))/,
@ -76,7 +76,7 @@ export default definePlugin({
}, },
// allows you to open mod view on yourself // allows you to open mod view on yourself
{ {
find: ".MEMBER_SAFETY,{modViewPanel:", find: 'action:"PRESS_MOD_VIEW",icon:',
predicate: () => settings.store.showModView, predicate: () => settings.store.showModView,
replacement: { replacement: {
match: /\i(?=\?null)/, match: /\i(?=\?null)/,

View file

@ -16,12 +16,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./styles.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { RelationshipStore } from "@webpack/common"; import { DateUtils, RelationshipStore, Text, TooltipContainer } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import { PropsWithChildren } from "react";
const formatter = new Intl.DateTimeFormat(undefined, {
month: "numeric",
day: "numeric",
year: "numeric",
});
const cl = classNameFactory("vc-sortFriendRequests-");
function getSince(user: User) {
return new Date(RelationshipStore.getSince(user.id));
}
const settings = definePluginSettings({ const settings = definePluginSettings({
showDates: { showDates: {
@ -48,28 +64,27 @@ export default definePlugin({
find: "#{intl::FRIEND_REQUEST_CANCEL}", find: "#{intl::FRIEND_REQUEST_CANCEL}",
replacement: { replacement: {
predicate: () => settings.store.showDates, predicate: () => settings.store.showDates,
match: /subText:(\i)(?<=user:(\i).+?)/, match: /(?<=\.listItemContents,children:\[)\(0,.+?(?=,\(0)(?<=user:(\i).+?)/,
replace: (_, subtext, user) => `subText:$self.makeSubtext(${subtext},${user})` replace: (children, user) => `$self.WrapperDateComponent({user:${user},children:${children}})`
} }
}], }],
wrapSort(comparator: Function, row: any) { wrapSort(comparator: Function, row: any) {
return row.type === 3 || row.type === 4 return row.type === 3 || row.type === 4
? -this.getSince(row.user) ? -getSince(row.user)
: comparator(row); : comparator(row);
}, },
getSince(user: User) { WrapperDateComponent: ErrorBoundary.wrap(({ user, children }: PropsWithChildren<{ user: User; }>) => {
return new Date(RelationshipStore.getSince(user.id)); const since = getSince(user);
},
makeSubtext(text: string, user: User) { return <div className={cl("wrapper")}>
const since = this.getSince(user); {children}
return ( {!isNaN(since.getTime()) && (
<Flex flexDirection="column" style={{ gap: 0, flexWrap: "wrap", lineHeight: "0.9rem" }}> <TooltipContainer text={DateUtils.dateFormat(since, "LLLL")} tooltipClassName={cl("tooltip")}>
<span>{text}</span> <Text variant="text-xs/normal" className={cl("date")}>{formatter.format(since)}</Text>
{!isNaN(since.getTime()) && <span>Received &mdash; {since.toDateString()}</span>} </TooltipContainer>
</Flex> )}
); </div>;
} })
}); });

View file

@ -0,0 +1,18 @@
.vc-sortFriendRequests-wrapper {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
margin-right: 0.5em;
}
.vc-sortFriendRequests-tooltip {
max-width: none;
white-space: nowrap;
}
.vc-sortFriendRequests-date {
color: var(--text-muted);
font-family: var(--font-code);
}

View file

@ -324,3 +324,10 @@ export interface DisplayProfileUtils {
getDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null; getDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null;
useDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null; useDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null;
} }
export interface DateUtils {
isSameDay(date1: Date, date2: Date): boolean;
calendarFormat(date: Date): string;
dateFormat(date: Date, format: string): string;
diffAsUnits(start: Date, end: Date, stopAtOneSecond?: boolean): Record<"days" | "hours" | "minutes" | "seconds", number>;
}

View file

@ -199,3 +199,10 @@ export const DisplayProfileUtils: t.DisplayProfileUtils = mapMangledModuleLazy(/
getDisplayProfile: filters.byCode(".getGuildMemberProfile("), getDisplayProfile: filters.byCode(".getGuildMemberProfile("),
useDisplayProfile: filters.byCode(/\[\i\.\i,\i\.\i],\(\)=>/) useDisplayProfile: filters.byCode(/\[\i\.\i,\i\.\i],\(\)=>/)
}); });
export const DateUtils: t.DateUtils = mapMangledModuleLazy("millisecondsInUnit:", {
calendarFormat: filters.byCode("sameElse"),
dateFormat: filters.byCode('":'),
isSameDay: filters.byCode("Math.abs(+"),
diffAsUnits: filters.byCode("days:0", "millisecondsInUnit")
});