Compare commits

...

14 Commits

Author SHA1 Message Date
V
5d1d054b7e MessageLinkEmbeds: Use api 2023-06-24 16:55:57 +02:00
V
a11d5a9111 Add ComponentUpdaterAPI 2023-06-24 16:46:09 +02:00
V
d7ac418e05 Fix some plugins displaying legacy discriminators (username#0000) 2023-06-23 18:09:43 +02:00
V
214c101740 Bump to v1.2.9 2023-06-22 22:45:28 +02:00
V
5a0e501829 Fix ValidUser, BetterFolders & MutualGroupDms
Co-authored-by: Juby210 <31005896+Juby210@users.noreply.github.com>
Co-authored-by: Amia <9750071+aamiaa@users.noreply.github.com>
2023-06-22 22:35:59 +02:00
V
92113da7c0 Fix Vencord Web 2023-06-21 21:50:28 +02:00
V
96f30a5359 Update README.md 2023-06-21 21:07:48 +02:00
V
ceb1f15188 utils: export missing members 2023-06-21 21:04:04 +02:00
V
626eb3613e [skip ci] WelcomeStickerPicker: Clean up settings types 2023-06-21 02:05:01 +02:00
V
3020fcc9bb [skip ci] Improve typings for settings.withPrivateSettings 2023-06-21 02:00:59 +02:00
heckeroncrack
bc0de3926c Improve uwuify (#1295)
Co-authored-by: V <vendicated@riseup.net>
2023-06-20 22:11:59 +02:00
Nuckyz
9820b79dfe SHC: show active now voice members; PermViewer: improve styling (#1314) 2023-06-20 22:11:50 +02:00
V
ab811470fc Update README.md 2023-06-18 21:27:52 +02:00
V
e4162e7bd5 PronounDB: Source pronouns from both PronounDB & Discord (#1301)
* PronounDB: Source pronouns from both PronounDB & Discord

* jdsjdsajjasiofigvjodsjigfdjiogegjnegjnersjn
2023-06-17 03:50:59 +02:00
27 changed files with 276 additions and 92 deletions

View File

@ -39,10 +39,9 @@ Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Ve
## Vencord Desktop
> **Warning**
> This is an alternative app. It currently doesn't support screensharing or keybinds. If you just want to install to the normal Discord Desktop app, scroll up
> This is an alternative app. It currently doesn't support keybinds and possibly some more features. If you just want to install to the normal Discord Desktop app, scroll up
As an alternative to the Discord Desktop app, Vencord also has its own standalone Desktop app that is snappier and lighter than Discord's official Desktop app. It is currently in beta and we have yet to implement some features like screensharing, but you can try the beta nonetheless
As an alternative to the Discord Desktop app, Vencord also has its own standalone Desktop app that is snappier and lighter than Discord's official Desktop app
[![Download Vencord Desktop](https://img.shields.io/github/v/release/Vencord/Desktop?label=Download%20Vencord%20Desktop&style=for-the-badge)](https://github.com/Vencord/Desktop#vencord-desktop)
@ -50,7 +49,7 @@ As an alternative to the Discord Desktop app, Vencord also has its own standalon
## Join our Support/Community Server
[![Vencord Discord Server](https://invidget.switchblade.xyz/D9uwnFnqmd?theme=dark)](https://discord.gg/D9uwnFnqmd)
https://discord.gg/D9uwnFnqmd
## Disclaimer

View File

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

View File

@ -0,0 +1,29 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 <https://www.gnu.org/licenses/>.
*/
import { proxyLazy } from "@utils/lazy";
const p = proxyLazy<typeof import("plugins/_api/componentUpdater").default>(() => Vencord.Plugins.plugins.ComponentUpdaterAPI as any);
/**
* Rerender a specific message
* @param messageId The id of the message to rerender
*/
export function updateMessageComponent(messageId: string) {
p.forceUpdaters.get(messageId)?.();
}

View File

@ -254,8 +254,12 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
}
}
export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) {
const definedSettings: DefinedSettings<D> = {
export function definePluginSettings<
Def extends SettingsDefinition,
Checks extends SettingsChecks<Def>,
PrivateSettings extends object = {}
>(def: Def, checks?: Checks) {
const definedSettings: DefinedSettings<Def, Checks, PrivateSettings> = {
get store() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return Settings.plugins[definedSettings.pluginName] as any;
@ -264,11 +268,11 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
).plugins[definedSettings.pluginName] as any,
def,
checks: checks ?? {},
checks: checks ?? {} as any,
pluginName: "",
withPrivateSettings<T>() {
return this as DefinedSettings<D, C> & { store: T; };
withPrivateSettings<T extends object>() {
return this as DefinedSettings<Def, Checks, T>;
}
};

View File

@ -18,6 +18,7 @@
import * as $Badges from "./Badges";
import * as $Commands from "./Commands";
import * as $ComponentUpdater from "./ComponentUpdater";
import * as $ContextMenu from "./ContextMenu";
import * as $DataStore from "./DataStore";
import * as $MemberListDecorators from "./MemberListDecorators";
@ -109,3 +110,8 @@ export const Notifications = $Notifications;
* An api allowing you to patch and add/remove items to/from context menus
*/
export const ContextMenu = $ContextMenu;
/**
* An api allowing you to update/rerender components
*/
export const ComponentUpdater = $ComponentUpdater;

View File

@ -0,0 +1,51 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types";
import { useEffect } from "@webpack/common";
import { Channel, Message } from "discord-types/general";
const forceUpdaters = new Map<string, () => void>();
function useUpdater(data: { channel: Channel; message: Message; }) {
const forceUpdater = useForceUpdater();
useEffect(() => {
forceUpdaters.set(data.message.id, forceUpdater);
return () => void forceUpdaters.delete(data.message.id);
}, [data.message.id]);
}
export default definePlugin({
name: "ComponentUpdaterAPI",
description: "API to update / force rerender several components, such as messages",
authors: [Devs.Ven],
patches: [{
find: ".renderContentOnly;",
replacement: {
match: /=(\i)\.renderContentOnly;/,
replace: "$&$self.useUpdater($1);"
}
}],
useUpdater,
forceUpdaters
});

View File

@ -36,7 +36,8 @@ function Guilds(props: {
// @ts-expect-error
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props);
const scrollerProps = res.props.children?.props?.children?.[1]?.props;
// TODO: Make this better
const scrollerProps = res.props.children?.props?.children?.props?.children?.[1]?.props;
if (scrollerProps?.children) {
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
if (servers) scrollerProps.children = servers;

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { updateMessageComponent } from "@api/ComponentUpdater";
import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
@ -28,7 +29,6 @@ import { find, findByCode, findByPropsLazy } from "@webpack";
import {
Button,
ChannelStore,
FluxDispatcher,
GuildStore,
MessageStore,
Parser,
@ -228,10 +228,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
delete msg.interaction;
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
.then(m => m && FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",
message: msg
}))
.then(m => m && updateMessageComponent(message.id))
);
continue;
}

View File

@ -78,7 +78,7 @@ export default definePlugin({
</Avatar>
<div className={ProfileListClasses.listRowContent}>
<div className={ProfileListClasses.listName}>{getGroupDMName(c)}</div>
<div className={GuildLabelClasses.guildNick}>{c.recipients.length} Members</div>
<div className={GuildLabelClasses.guildNick}>{c.recipients.length + 1} Members</div>
</div>
</Clickable>
));

View File

@ -19,10 +19,12 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ContextMenu, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { settings } from "..";
import { cl, getPermissionDescription, getPermissionString } from "../utils";
import { PermissionAllowedIcon, PermissionDefaultIcon, PermissionDeniedIcon } from "./icons";
@ -108,7 +110,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
<div
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
onContextMenu={e => {
if (permission.type === PermissionType.Role)
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
ContextMenu.open(e, () => (
<RoleContextMenu
guild={guild}
@ -135,7 +137,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
permission.type === PermissionType.Role
? role?.name || "Unknown Role"
: permission.type === PermissionType.User
? user?.tag || "Unknown User"
? (user && getUniqueUsername(user)) || "Unknown User"
: (
<Flex style={{ gap: "0.2em", justifyItems: "center" }}>
@owner

View File

@ -63,6 +63,7 @@
grid-area: list;
display: flex;
flex-direction: column;
gap: 2px;
border-right: 2px solid var(--background-modifier-active);
}
@ -77,6 +78,15 @@
padding: 8px 5px;
cursor: pointer;
width: 230px;
border-radius: 5px;
}
.vc-permviewer-perms-list-item:hover {
background-color: var(--background-modifier-hover);
}
.vc-permviewer-perms-list-item-active {
background-color: var(--background-modifier-selected);
}
.vc-permviewer-perms-list-item > div {
@ -85,11 +95,6 @@
overflow: hidden;
}
.vc-permviewer-perms-list-item-active {
background-color: var(--background-modifier-selected);
border-radius: 5px;
}
.vc-permviewer-perms-role-circle {
border-radius: 50%;
width: 12px;

View File

@ -52,7 +52,7 @@ export default definePlugin({
find: ".userTagNoNickname",
replacement: {
match: /=(\i)\.pronouns/,
replace: "=$self.useProfilePronouns($1.user.id)"
replace: "=$self.useProfilePronouns($1.user.id,$1.pronouns)"
}
},
// Patch the profile modal username header to use our pronoun hook instead of Discord's pronouns
@ -60,7 +60,7 @@ export default definePlugin({
find: ".USER_PROFILE_ACTIVITY",
replacement: {
match: /\).showPronouns/,
replace: ").showPronouns||true;const vcPronounce=$self.useProfilePronouns(arguments[0].user.id);if(arguments[0].displayProfile)arguments[0].displayProfile.pronouns=vcPronounce"
replace: ").showPronouns||true;const vcPronounce=$self.useProfilePronouns(arguments[0].user.id,arguments[0].displayProfile?.pronouns);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce"
}
}
],

View File

@ -19,17 +19,26 @@
import { Settings } from "@api/Settings";
import { VENCORD_USER_AGENT } from "@utils/constants";
import { debounce } from "@utils/debounce";
import { getCurrentChannel } from "@utils/discord";
import { useAwaiter } from "@utils/react";
import { findStoreLazy } from "@webpack";
import { UserStore } from "@webpack/common";
import { settings } from "./settings";
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
const UserProfileStore = findStoreLazy("UserProfileStore");
export const enum PronounsFormat {
Lowercase = "LOWERCASE",
Capitalized = "CAPITALIZED"
}
export const enum PronounSource {
PreferPDB,
PreferDiscord
}
// A map of cached pronouns so the same request isn't sent twice
const cache: Record<string, PronounCode> = {};
// A map of ids and callbacks that should be triggered on fetch
@ -46,21 +55,29 @@ const bulkFetch = debounce(async () => {
}
});
export function useFormattedPronouns(id: string): string | null {
const [result] = useAwaiter(() => fetchPronouns(id), {
fallbackValue: getCachedPronouns(id),
function getDiscordPronouns(id: string) {
return (
UserProfileStore.getGuildMemberProfile(id, getCurrentChannel()?.guild_id)?.pronouns
|| UserProfileStore.getUserProfile(id)?.pronouns
);
}
export function useFormattedPronouns(id: string, discordPronouns: string = getDiscordPronouns(id)): string | null {
const [result] = useAwaiter(() => fetchPronouns(id, discordPronouns), {
fallbackValue: getCachedPronouns(id, discordPronouns),
onError: e => console.error("Fetching pronouns failed: ", e)
});
// If the result is present and not "unspecified", and there is a mapping for the code, then return the mappings
if (result && result !== "unspecified" && PronounMapping[result])
return formatPronouns(result);
if (result && result !== "unspecified")
return Object.hasOwn(PronounMapping, result)
? formatPronouns(result) // PronounDB
: result; // Discord
return null;
}
export function useProfilePronouns(id: string) {
const pronouns = useFormattedPronouns(id);
export function useProfilePronouns(id: string, discordPronouns: string) {
const pronouns = useFormattedPronouns(id, discordPronouns);
if (!settings.store.showInProfile) return null;
if (!settings.store.showSelf && id === UserStore.getCurrentUser().id) return null;
@ -70,22 +87,28 @@ export function useProfilePronouns(id: string) {
// Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string): PronounCode | null {
return cache[id] ?? null;
export function getCachedPronouns(id: string, discordPronouns: string): string | null {
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
return discordPronouns;
const cached = cache[id];
if (cached && cached !== "unspecified") return cached;
return discordPronouns || cached || null;
}
// Fetches the pronouns for one id, returning a promise that resolves if it was cached, or once the request is completed
export function fetchPronouns(id: string): Promise<PronounCode> {
export function fetchPronouns(id: string, discordPronouns: string): Promise<string> {
return new Promise(res => {
// If cached, return the cached pronouns
if (id in cache) res(getCachedPronouns(id)!);
const cached = getCachedPronouns(id, discordPronouns);
if (cached) return res(cached);
// If there is already a request added, then just add this callback to it
else if (id in requestQueue) requestQueue[id].push(res);
if (id in requestQueue) return requestQueue[id].push(res);
// If not already added, then add it and call the debounced function to make sure the request gets executed
else {
requestQueue[id] = [res];
bulkFetch();
}
requestQueue[id] = [res];
bulkFetch();
});
}
@ -116,7 +139,7 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
}
}
export function formatPronouns(pronouns: PronounCode): string {
export function formatPronouns(pronouns: string): string {
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
// For capitalized pronouns, just return the mapping (it is by default capitalized)
if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns];

View File

@ -19,7 +19,7 @@
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
import { PronounsFormat } from "./pronoundbUtils";
import { PronounsFormat, PronounSource } from "./pronoundbUtils";
export const settings = definePluginSettings({
pronounsFormat: {
@ -37,6 +37,21 @@ export const settings = definePluginSettings({
}
]
},
pronounSource: {
type: OptionType.SELECT,
description: "Where to source pronouns from",
options: [
{
label: "Prefer PronounDB, fall back to Discord",
value: PronounSource.PreferPDB,
default: true
},
{
label: "Prefer Discord, fall back to PronounDB (might lead to inconsistency between pronouns in chat and profile)",
value: PronounSource.PreferDiscord
}
]
},
showSelf: {
type: OptionType.BOOLEAN,
description: "Enable or disable showing pronouns for the current user",

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getUniqueUsername, openUserProfile } from "@utils/discord";
import { UserUtils } from "@webpack/common";
import settings from "./settings";
@ -43,11 +44,19 @@ export async function onRelationshipRemove({ relationship: { type, id } }: Relat
switch (type) {
case RelationshipType.FRIEND:
if (settings.store.friends)
notify(`${user.tag} removed you as a friend.`, user.getAvatarURL(undefined, undefined, false));
notify(
`${getUniqueUsername(user)} removed you as a friend.`,
user.getAvatarURL(undefined, undefined, false),
() => openUserProfile(user.id)
);
break;
case RelationshipType.FRIEND_REQUEST:
if (settings.store.friendRequestCancels)
notify(`A friend request from ${user.tag} has been removed.`, user.getAvatarURL(undefined, undefined, false));
notify(
`A friend request from ${getUniqueUsername(user)} has been removed.`,
user.getAvatarURL(undefined, undefined, false),
() => openUserProfile(user.id)
);
break;
}
}

View File

@ -18,6 +18,7 @@
import { DataStore, Notices } from "@api/index";
import { showNotification } from "@api/Notifications";
import { getUniqueUsername, openUserProfile } from "@utils/discord";
import { ChannelStore, GuildMemberStore, GuildStore, RelationshipStore, UserStore, UserUtils } from "@webpack/common";
import settings from "./settings";
@ -69,7 +70,11 @@ export async function syncAndRunChecks() {
const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user)
notify(`You are no longer friends with ${user.tag}.`, user.getAvatarURL(undefined, undefined, false));
notify(
`You are no longer friends with ${getUniqueUsername(user)}.`,
user.getAvatarURL(undefined, undefined, false),
() => openUserProfile(user.id)
);
}
}
@ -79,20 +84,25 @@ export async function syncAndRunChecks() {
const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user)
notify(`Friend request from ${user.tag} has been revoked.`, user.getAvatarURL(undefined, undefined, false));
notify(
`Friend request from ${getUniqueUsername(user)} has been revoked.`,
user.getAvatarURL(undefined, undefined, false),
() => openUserProfile(user.id)
);
}
}
}
}
export function notify(text: string, icon?: string) {
export function notify(text: string, icon?: string, onClick?: () => void) {
if (settings.store.notices)
Notices.showNotice(text, "OK", () => Notices.popNotice());
showNotification({
title: "Relationship Notifier",
body: text,
icon
icon,
onClick
});
}

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack";
@ -24,7 +25,7 @@ import { Alerts, moment, Timestamp, UserStore } from "@webpack/common";
import { Review, ReviewType } from "../entities";
import { deleteReview, reportReview } from "../reviewDbApi";
import { settings } from "../settings";
import { canDeleteReview, cl, openUserProfileModal, showToast } from "../utils";
import { canDeleteReview, cl, showToast } from "../utils";
import { DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge";
@ -49,7 +50,7 @@ export default LazyComponent(() => {
return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) {
function openModal() {
openUserProfileModal(review.sender.discordID);
openUserProfile(review.sender.discordID);
}
function delReview() {

View File

@ -20,24 +20,13 @@ import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { FluxDispatcher, React, SelectedChannelStore, Toasts, UserUtils } from "@webpack/common";
import { React, Toasts } from "@webpack/common";
import { Review, UserType } from "./entities";
import { settings } from "./settings";
export const cl = classNameFactory("vc-rdb-");
export async function openUserProfileModal(userId: string) {
await UserUtils.fetchUser(userId);
await FluxDispatcher.dispatch({
type: "USER_PROFILE_MODAL_OPEN",
userId,
channelId: SelectedChannelStore.getChannelId(),
analyticsLocation: "Explosive Hotel"
});
}
export function authorize(callback?: any) {
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");

View File

@ -419,6 +419,14 @@ export default definePlugin({
match: /(?<=getChannels\(\i)(?=\))/,
replace: ",true"
}
},
{
find: '.displayName="NowPlayingViewStore"',
replacement: {
// Make active now voice states on hiddenl channels
match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/,
replace: "$1"
}
}
],

View File

@ -19,13 +19,13 @@
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { openUserProfile } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { GuildMemberStore, React, RelationshipStore, SelectedChannelStore } from "@webpack/common";
import { GuildMemberStore, React, RelationshipStore } from "@webpack/common";
import { User } from "discord-types/general";
const Avatar = findByCodeLazy(".typingIndicatorRef", "svg");
const openProfile = findByCodeLazy("friendToken", "USER_PROFILE_MODAL_OPEN");
const settings = definePluginSettings({
showAvatars: {
@ -64,15 +64,7 @@ const TypingUser = ErrorBoundary.wrap(function ({ user, guildId }: Props) {
<strong
role="button"
onClick={() => {
openProfile({
userId: user.id,
guildId,
channelId: SelectedChannelStore.getChannelId(),
analyticsLocation: {
page: guildId ? "Guild Channel" : "DM Channel",
section: "Profile Popout"
}
});
openUserProfile(user.id);
}}
style={{
display: "grid",

View File

@ -65,6 +65,7 @@ const replacements = [
["stupid", "baka"],
["what", "nani"],
["meow", "nya~"],
["hello", "hewwo"],
];
const settings = definePluginSettings({

View File

@ -24,7 +24,7 @@ import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { UserStore, useState } from "@webpack/common";
import type { User } from "discord-types/general";
import type { ComponentType } from "react";
import type { ComponentType, ReactNode } from "react";
const fetching = new Set<string>();
const queue = new Queue(5);
@ -36,7 +36,7 @@ interface MentionProps {
channelId?: string;
content: any;
};
parse: (content: any, props: MentionProps["props"]) => string[];
parse: (content: any, props: MentionProps["props"]) => ReactNode;
props: {
key: string;
formatInline: boolean;
@ -72,7 +72,7 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
>
<span
onMouseEnter={() => {
const mention = children?.[0];
const mention = children?.[0]?.props?.children;
if (typeof mention !== "string") return;
const id = mention.match(/<@(\d+)>/)?.[1];

View File

@ -44,7 +44,10 @@ const settings = definePluginSettings({
],
description: "Choose the greet mode"
}
});
}).withPrivateSettings<{
multiGreetChoices?: string[];
unholyMultiGreetEnabled?: boolean;
}>();
const MessageActions = findByPropsLazy("sendGreetMessage");
@ -73,7 +76,7 @@ function greet(channel: Channel, message: Message, stickers: string[]) {
function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], message: Message, channel: Channel; }) {
const s = settings.use(["greetMode", "multiGreetChoices"] as any) as { greetMode: GreetMode, multiGreetChoices: string[]; };
const s = settings.use(["greetMode", "multiGreetChoices"]);
const { greetMode, multiGreetChoices = [] } = s;
return (
@ -112,7 +115,7 @@ function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], messag
))}
</Menu.MenuGroup>
{!(settings.store as any).unholyMultiGreetEnabled ? null : (
{!settings.store.unholyMultiGreetEnabled ? null : (
<>
<Menu.MenuSeparator />

View File

@ -17,9 +17,9 @@
*/
import { MessageObject } from "@api/MessageEvents";
import { findByPropsLazy, findLazy } from "@webpack";
import { ChannelStore, ComponentDispatch, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, SelectedChannelStore } from "@webpack/common";
import { Guild, Message } from "discord-types/general";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { ChannelStore, ComponentDispatch, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, SelectedChannelStore, SelectedGuildStore, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
@ -99,3 +99,28 @@ export function openImageModal(url: string, props?: Partial<React.ComponentProps
</ModalRoot>
));
}
const openProfile = findByCodeLazy("friendToken", "USER_PROFILE_MODAL_OPEN");
export async function openUserProfile(id: string) {
const user = await UserUtils.fetchUser(id);
if (!user) throw new Error("No such user: " + id);
const guildId = SelectedGuildStore.getGuildId();
openProfile({
userId: id,
guildId,
channelId: SelectedChannelStore.getChannelId(),
analyticsLocation: {
page: guildId ? "Guild Channel" : "DM Channel",
section: "Profile Popout"
}
});
}
/**
* Get the unique username for a user. Returns user.username for pomelo people, user.tag otherwise
*/
export function getUniqueUsername(user: User) {
return user.discriminator === "0" ? user.username : user.tag;
}

View File

@ -31,4 +31,5 @@ export * from "./onceDefined";
export * from "./onlyOnce";
export * from "./patches";
export * from "./Queue";
export * from "./react";
export * from "./text";

View File

@ -260,25 +260,29 @@ type SettingsStore<D extends SettingsDefinition> = {
};
/** An instance of defined plugin settings */
export interface DefinedSettings<D extends SettingsDefinition = SettingsDefinition, C extends SettingsChecks<D> = {}> {
export interface DefinedSettings<
Def extends SettingsDefinition = SettingsDefinition,
Checks extends SettingsChecks<Def> = {},
PrivateSettings extends object = {}
> {
/** Shorthand for `Vencord.Settings.plugins.PluginName`, but with typings */
store: SettingsStore<D>;
store: SettingsStore<Def> & PrivateSettings;
/**
* React hook for getting the settings for this plugin
* @param filter optional filter to avoid rerenders for irrelavent settings
*/
use<F extends Extract<keyof D, string>>(filter?: F[]): Pick<SettingsStore<D>, F>;
use<F extends Extract<keyof Def | keyof PrivateSettings, string>>(filter?: F[]): Pick<SettingsStore<Def> & PrivateSettings, F>;
/** Definitions of each setting */
def: D;
def: Def;
/** Setting methods with return values that could rely on other settings */
checks: C;
checks: Checks;
/**
* Name of the plugin these settings belong to,
* will be an empty string until plugin is initialized
*/
pluginName: string;
withPrivateSettings<T>(): this & { store: T; };
withPrivateSettings<T extends object>(): DefinedSettings<Def, Checks, T>;
}
export type PartialExcept<T, R extends keyof T> = Partial<T> & Required<Pick<T, R>>;

View File

@ -20,14 +20,23 @@ import { Channel } from "discord-types/general";
import { FluxDispatcher, FluxEvents } from "./utils";
type GenericFunction = (...args: any[]) => any;
export class FluxStore {
constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial<Record<FluxEvents, (data: any) => void>>);
addChangeListener(callback: () => void): void;
addReactChangeListener(callback: () => void): void;
removeChangeListener(callback: () => void): void;
removeReactChangeListener(callback: () => void): void;
emitChange(): void;
getDispatchToken(): string;
getName(): string;
initialize(): void;
initializeIfNeeded(): void;
registerActionHandlers: GenericFunction;
syncWith: GenericFunction;
waitFor: GenericFunction;
__getLocalVars(): Record<string, any>;
}