MoreUserTags Chat

This commit is contained in:
thororen1234 2025-05-02 21:00:56 -04:00
parent 61e75e1d89
commit 2613ec170e
No known key found for this signature in database
17 changed files with 668 additions and 228 deletions

View 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()");

View 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);
},
});

View 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"
}
});

View 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;
}

View 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;
};

View file

@ -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>