Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

moreUserTags: attempt to rewrite #3170

Draft
wants to merge 27 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7740d9a
moreUserTags: rewrite
henmalib Jan 27, 2025
b74e5e2
moreUserTags: readable tags/devs
henmalib Jan 27, 2025
97f540c
no more lint fails for you, sadan
henmalib Jan 27, 2025
c219cdc
moreUserTags: removed 'add tags' patch
henmalib Jan 27, 2025
f9ab7d3
moreUserTags: rename obj/format
henmalib Jan 28, 2025
ae1ed25
moreUserTags: memberList
henmalib Jan 28, 2025
9ea2f6f
moreUserTags: profile support
henmalib Jan 28, 2025
6ef9712
moreUserTags: better typing
henmalib Jan 29, 2025
4370dd4
Merge branch 'main' into tags
henmalib Jan 29, 2025
7ea9677
moreUserTags: use decorations
henmalib Jan 29, 2025
3e855ff
Merge branch 'tags' of github.com:henmalib/Vencord into tags
henmalib Jan 29, 2025
f22d9fc
moreUserTags: revert to patching
henmalib Jan 29, 2025
142fd3e
Merge branch 'main' into tags
henmalib Feb 3, 2025
5a6ea90
moreUserTags: use decorations
henmalib Feb 3, 2025
e2b0e25
moreUserTags: cleanup
henmalib Feb 3, 2025
bf181ec
Merge branch 'dev' into tags
henmalib Feb 4, 2025
ac1c720
moreUserTags: change classes
henmalib Feb 4, 2025
4810df4
moreUserTags: fix center offset
henmalib Feb 4, 2025
8d1cab9
Fix alignment for tags in messages
Nuckyz Feb 4, 2025
a04a97c
moreUserTags: generate settings if not present
henmalib Feb 4, 2025
06c2ba5
moreUserTags: removed unused const
henmalib Feb 4, 2025
90f2e46
moreUserTags: actually use settings
henmalib Feb 5, 2025
d474337
moreUserTags: oops
henmalib Feb 5, 2025
e30cc47
Merge branch 'dev' into tags
henmalib Feb 5, 2025
29512a5
moreUserTags: use proper height
henmalib Feb 5, 2025
97676c6
balls
henmalib Feb 5, 2025
ddd6668
moreUserTags: use classFactory
henmalib Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/plugins/moreUserTags/consts.ts
Original file line number Diff line number Diff line change
@@ -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()");
178 changes: 178 additions & 0 deletions src/plugins/moreUserTags/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* 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 { 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],
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
},
],
},

// User profile
// TODO: replace with API
{
find: ".clickableUsername",
replacement: {
match: /null!=(\i)(?=.{0,100}type:\i)/,
replace: "($1=$self.getTag({...arguments[0],channelId:$self.getChannelId(),isChat:false,origType:$1}),$1!==null)"
}
}
],
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();
},
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: props.channel,
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 changes: 81 additions & 0 deletions src/plugins/moreUserTags/settings.tsx
Original file line number Diff line number Diff line change
@@ -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"
}
});
12 changes: 12 additions & 0 deletions src/plugins/moreUserTags/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.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;
}
32 changes: 32 additions & 0 deletions src/plugins/moreUserTags/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
4 changes: 4 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "jamesbt365",
id: 158567567487795200n,
},
hen: {
id: 279266228151779329n,
name: "Hen"
}
} satisfies Record<string, Dev>);

// iife so #__PURE__ works correctly
Expand Down