mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-24 05:47:00 -04:00
MoreUserTags Chat
This commit is contained in:
parent
61e75e1d89
commit
2613ec170e
17 changed files with 668 additions and 228 deletions
63
src/equicordplugins/moreUserTags/consts.ts
Normal file
63
src/equicordplugins/moreUserTags/consts.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findByCodeLazy, findLazy } from "@webpack";
|
||||
import { GuildStore } from "@webpack/common";
|
||||
import { RC } from "@webpack/types";
|
||||
import { Channel, Guild, Message, User } from "discord-types/general";
|
||||
|
||||
import type { ITag } from "./types";
|
||||
|
||||
export const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
|
||||
export const tags = [
|
||||
{
|
||||
name: "WEBHOOK",
|
||||
displayName: "Webhook",
|
||||
description: "Messages sent by webhooks",
|
||||
condition: isWebhook
|
||||
}, {
|
||||
name: "OWNER",
|
||||
displayName: "Owner",
|
||||
description: "Owns the server",
|
||||
condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id
|
||||
}, {
|
||||
name: "ADMINISTRATOR",
|
||||
displayName: "Admin",
|
||||
description: "Has the administrator permission",
|
||||
permissions: ["ADMINISTRATOR"]
|
||||
}, {
|
||||
name: "MODERATOR_STAFF",
|
||||
displayName: "Staff",
|
||||
description: "Can manage the server, channels or roles",
|
||||
permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"]
|
||||
}, {
|
||||
name: "MODERATOR",
|
||||
displayName: "Mod",
|
||||
description: "Can manage messages or kick/ban people",
|
||||
permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"]
|
||||
}, {
|
||||
name: "VOICE_MODERATOR",
|
||||
displayName: "VC Mod",
|
||||
description: "Can manage voice chats",
|
||||
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
||||
}, {
|
||||
name: "CHAT_MODERATOR",
|
||||
displayName: "Chat Mod",
|
||||
description: "Can timeout people",
|
||||
permissions: ["MODERATE_MEMBERS"]
|
||||
}
|
||||
] as const satisfies ITag[];
|
||||
|
||||
export const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number | null, className?: string, useRemSizes?: boolean; }> & { Types: Record<string, number>; };
|
||||
|
||||
// PermissionStore.computePermissions will not work here since it only gets permissions for the current user
|
||||
export const computePermissions: (options: {
|
||||
user?: { id: string; } | string | null;
|
||||
context?: Guild | Channel | null;
|
||||
overwrites?: Channel["permissionOverwrites"] | null;
|
||||
checkElevated?: boolean /* = true */;
|
||||
excludeGuildPermissions?: boolean /* = false */;
|
||||
}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()");
|
183
src/equicordplugins/moreUserTags/index.tsx
Normal file
183
src/equicordplugins/moreUserTags/index.tsx
Normal file
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentChannel, getIntlMessage } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
import { ChannelStore, GuildStore, PermissionsBits, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
import { Channel, Message, User } from "discord-types/general";
|
||||
|
||||
import { computePermissions, Tag, tags } from "./consts";
|
||||
import { settings } from "./settings";
|
||||
import { TagSettings } from "./types";
|
||||
|
||||
const cl = classNameFactory("vc-mut-");
|
||||
|
||||
const genTagTypes = () => {
|
||||
let i = 100;
|
||||
const obj = {};
|
||||
|
||||
for (const { name } of tags) {
|
||||
obj[name] = ++i;
|
||||
obj[i] = name;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "MoreUserTags",
|
||||
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
|
||||
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, Devs.hen],
|
||||
dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"],
|
||||
settings,
|
||||
patches: [
|
||||
// Make discord actually use our tags
|
||||
{
|
||||
find: ".STAFF_ONLY_DM:",
|
||||
replacement: [{
|
||||
match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})default:(\i)=/,
|
||||
replace: "default:$2=$self.getTagText($self.localTags[$1]);",
|
||||
}, {
|
||||
match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})\.BOT:(?=default:)/,
|
||||
replace: "$&return null;",
|
||||
predicate: () => settings.store.dontShowBotTag
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
start() {
|
||||
const tagSettings = settings.store.tagSettings || {} as TagSettings;
|
||||
for (const tag of Object.values(tags)) {
|
||||
tagSettings[tag.name] ??= {
|
||||
showInChat: true,
|
||||
showInNotChat: true,
|
||||
text: tag.displayName
|
||||
};
|
||||
}
|
||||
|
||||
settings.store.tagSettings = tagSettings;
|
||||
},
|
||||
localTags: genTagTypes(),
|
||||
getChannelId() {
|
||||
return SelectedChannelStore.getChannelId();
|
||||
},
|
||||
renderNicknameIcon(props) {
|
||||
const tagId = this.getTag({
|
||||
user: UserStore.getUser(props.userId),
|
||||
channel: ChannelStore.getChannel(this.getChannelId()),
|
||||
channelId: this.getChannelId(),
|
||||
isChat: false
|
||||
});
|
||||
|
||||
return tagId && <Tag
|
||||
type={tagId}
|
||||
verified={false}>
|
||||
</Tag>;
|
||||
|
||||
},
|
||||
renderMessageDecoration(props) {
|
||||
const tagId = this.getTag({
|
||||
message: props.message,
|
||||
user: UserStore.getUser(props.message.author.id),
|
||||
channelId: props.message.channel_id,
|
||||
isChat: false
|
||||
});
|
||||
|
||||
return tagId && <Tag
|
||||
useRemSizes={true}
|
||||
className={cl("message-tag", props.message.author.isVerifiedBot() && "message-verified")}
|
||||
type={tagId}
|
||||
verified={false}>
|
||||
</Tag>;
|
||||
},
|
||||
renderMemberListDecorator(props) {
|
||||
const tagId = this.getTag({
|
||||
user: props.user,
|
||||
channel: getCurrentChannel(),
|
||||
channelId: this.getChannelId(),
|
||||
isChat: false
|
||||
});
|
||||
|
||||
return tagId && <Tag
|
||||
type={tagId}
|
||||
verified={false}>
|
||||
</Tag>;
|
||||
},
|
||||
|
||||
getTagText(tagName: string) {
|
||||
if (!tagName) return getIntlMessage("APP_TAG");
|
||||
const tag = tags.find(({ name }) => tagName === name);
|
||||
if (!tag) return tagName || getIntlMessage("APP_TAG");
|
||||
|
||||
return settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
|
||||
},
|
||||
|
||||
getTag({
|
||||
message, user, channelId, isChat, channel
|
||||
}: {
|
||||
message?: Message,
|
||||
user?: User & { isClyde(): boolean; },
|
||||
channel?: Channel & { isForumPost(): boolean; isMediaPost(): boolean; },
|
||||
channelId?: string;
|
||||
isChat?: boolean;
|
||||
}): number | null {
|
||||
const settings = this.settings.store;
|
||||
|
||||
if (!user) return null;
|
||||
if (isChat && user.id === "1") return null;
|
||||
if (user.isClyde()) return null;
|
||||
if (user.bot && settings.dontShowForBots) return null;
|
||||
|
||||
channel ??= ChannelStore.getChannel(channelId!) as any;
|
||||
if (!channel) return null;
|
||||
|
||||
const perms = this.getPermissions(user, channel);
|
||||
|
||||
for (const tag of tags) {
|
||||
if (isChat && !settings.tagSettings[tag.name].showInChat)
|
||||
continue;
|
||||
if (!isChat && !settings.tagSettings[tag.name].showInNotChat)
|
||||
continue;
|
||||
|
||||
// If the owner tag is disabled, and the user is the owner of the guild,
|
||||
// avoid adding other tags because the owner will always match the condition for them
|
||||
if (
|
||||
(tag.name !== "OWNER" &&
|
||||
GuildStore.getGuild(channel?.guild_id)?.ownerId ===
|
||||
user.id &&
|
||||
isChat &&
|
||||
!settings.tagSettings.OWNER.showInChat) ||
|
||||
(!isChat &&
|
||||
!settings.tagSettings.OWNER.showInNotChat)
|
||||
)
|
||||
continue;
|
||||
|
||||
if ("permissions" in tag ?
|
||||
tag.permissions.some(perm => perms.includes(perm)) :
|
||||
tag.condition(message!, user, channel)) {
|
||||
|
||||
return this.localTags[tag.name];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
getPermissions(user: User, channel: Channel): string[] {
|
||||
const guild = GuildStore.getGuild(channel?.guild_id);
|
||||
if (!guild) return [];
|
||||
|
||||
const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
|
||||
return Object.entries(PermissionsBits)
|
||||
.map(([perm, permInt]) =>
|
||||
permissions & permInt ? perm : ""
|
||||
)
|
||||
.filter(Boolean);
|
||||
},
|
||||
});
|
81
src/equicordplugins/moreUserTags/settings.tsx
Normal file
81
src/equicordplugins/moreUserTags/settings.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { Card, Flex, Forms, Switch, TextInput, Tooltip } from "@webpack/common";
|
||||
|
||||
import { Tag, tags } from "./consts";
|
||||
import { TagSettings } from "./types";
|
||||
|
||||
function SettingsComponent() {
|
||||
const tagSettings = settings.store.tagSettings as TagSettings;
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
{tags.map(t => (
|
||||
<Card key={t.name} style={{ padding: "1em 1em 0" }}>
|
||||
<Forms.FormTitle style={{ width: "fit-content" }}>
|
||||
<Tooltip text={t.description}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{t.displayName} Tag <Tag type={Tag.Types[t.name]} />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Forms.FormTitle>
|
||||
|
||||
<TextInput
|
||||
type="text"
|
||||
value={tagSettings[t.name]?.text ?? t.displayName}
|
||||
placeholder={`Text on tag (default: ${t.displayName})`}
|
||||
onChange={v => tagSettings[t.name].text = v}
|
||||
className={Margins.bottom16}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
value={tagSettings[t.name]?.showInChat ?? true}
|
||||
onChange={v => tagSettings[t.name].showInChat = v}
|
||||
hideBorder
|
||||
>
|
||||
Show in messages
|
||||
</Switch>
|
||||
|
||||
<Switch
|
||||
value={tagSettings[t.name]?.showInNotChat ?? true}
|
||||
onChange={v => tagSettings[t.name].showInNotChat = v}
|
||||
hideBorder
|
||||
>
|
||||
Show in member list and profiles
|
||||
</Switch>
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
dontShowForBots: {
|
||||
description: "Don't show extra tags for bots (excluding webhooks)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
dontShowBotTag: {
|
||||
description: "Only show extra tags for bots / Hide [APP] text",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
restartNeeded: true
|
||||
},
|
||||
tagSettings: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: SettingsComponent,
|
||||
description: "fill me"
|
||||
}
|
||||
});
|
20
src/equicordplugins/moreUserTags/styles.css
Normal file
20
src/equicordplugins/moreUserTags/styles.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.vc-mut-message-tag {
|
||||
/* Remove default margin from tags in messages */
|
||||
margin-top: unset !important;
|
||||
|
||||
/* Align with Discord default tags in messages */
|
||||
position: relative;
|
||||
bottom: 0.01em;
|
||||
}
|
||||
|
||||
.vc-mut-message-verified {
|
||||
height: 1rem !important;
|
||||
}
|
||||
|
||||
span[class*="botTagCozy"][data-moreTags-darkFg="true"]>svg>path {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
span[class*="botTagCozy"][data-moreTags-darkFg="false"]>svg>path {
|
||||
fill: #fff;
|
||||
}
|
32
src/equicordplugins/moreUserTags/types.ts
Normal file
32
src/equicordplugins/moreUserTags/types.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Permissions } from "@webpack/types";
|
||||
import type { Channel, Message, User } from "discord-types/general";
|
||||
|
||||
import { tags } from "./consts";
|
||||
|
||||
export type ITag = {
|
||||
// name used for identifying, must be alphanumeric + underscores
|
||||
name: string;
|
||||
// name shown on the tag itself, can be anything probably; automatically uppercase'd
|
||||
displayName: string;
|
||||
description: string;
|
||||
} & ({
|
||||
permissions: Permissions[];
|
||||
} | {
|
||||
condition?(message: Message | null, user: User, channel: Channel): boolean;
|
||||
});
|
||||
|
||||
export interface TagSetting {
|
||||
text: string;
|
||||
showInChat: boolean;
|
||||
showInNotChat: boolean;
|
||||
}
|
||||
|
||||
export type TagSettings = {
|
||||
[k in typeof tags[number]["name"]]: TagSetting;
|
||||
};
|
|
@ -125,7 +125,14 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{renderBody ? richBody ?? <p className="toastnotifications-notification-p">{body}</p> : null}
|
||||
{renderBody ? (
|
||||
richBody ?? (
|
||||
<p className="toastnotifications-notification-p">
|
||||
{body.length > 500 ? body.slice(0, 500) + "..." : body}
|
||||
</p>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{PluginSettings.store.renderImages && image && <img className="toastnotifications-notification-img" src={image} alt="ToastNotification Image" />}
|
||||
{footer && <p className="toastnotifications-notification-footer">{`${attachments} attachment${attachments > 1 ? "s" : ""} ${attachments > 1 ? "were" : "was"} sent.`}</p>}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue