Remove FriendTags & IRememberYou

This commit is contained in:
RobinRMC 2024-11-25 12:23:49 +01:00 committed by GitHub
parent ef7fb4304c
commit 22b2824df2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,229 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { ExpandableHeader } from "@components/ExpandableHeader";
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Menu, RelationshipStore, Text, TextInput, useEffect, UserStore, useState } from "@webpack/common";
const tagStoreName = "vc-friendtags-tags";
function parseUsertags(text: string): string[] {
const regex = /&(\w+)/g;
const matches = text.match(regex);
if (matches) {
const tags = matches.map(match => match.substring(1));
return tags.filter(tag => tag !== "");
} else {
return [];
}
}
function queryFriendTags(query) {
GetData();
const tags = parseUsertags(query).map(e => e.toLowerCase());
const filteredTagObjects = SavedData.filter(data => data.tagName.length && data.userIds.length).filter(data => tags.some(tag => tag === data.tagName));
if (filteredTagObjects.length === 0) return [];
const users = Array.from(new Set([...ChannelStore.getDMUserIds(), ...RelationshipStore.getFriendIDs()])).filter(user => filteredTagObjects.every(tag => tag.userIds.includes(user)));
const response = users.map(user => {
const userObject: any = UserStore.getUser(user);
return (
{
"type": "USER",
"record": userObject,
"score": 20,
"comparator": userObject.globalName || userObject.username,
"sortable": userObject.globalName || userObject.username
}
);
});
return response;
}
let SavedData: UserTagData[] = [];
interface UserTagData {
tagName: string;
userIds: string[];
}
async function SetData() {
const fetchData = await DataStore.get(tagStoreName);
if (SavedData !== fetchData) {
await DataStore.set(tagStoreName, JSON.stringify(SavedData));
}
return true;
}
async function GetData() {
const fetchData = await DataStore.get(tagStoreName);
if (!fetchData) {
DataStore.set(tagStoreName, JSON.stringify([]));
SavedData = [];
return;
}
SavedData = JSON.parse(fetchData);
}
function TagConfigCard(props) {
const { tag } = props;
const [tagName, setTagName] = useState(tag.tagName);
const [userIds, setUserIDs] = useState(tag.userIds.join(", "));
const update = useForceUpdater();
useEffect(() => {
const dataTag = SavedData.find(obj => obj.tagName === tag.tagName);
if (dataTag) {
dataTag.tagName = tagName;
}
SetData();
update();
}, [tagName]);
useEffect(() => {
const dataTag = SavedData.find(obj => obj.userIds === tag.userIds);
if (dataTag) {
dataTag.userIds = userIds.split(", ");
}
SetData();
update();
}, [userIds]);
return (
<>
<Text variant={"heading-md/normal"}>Name</Text>
<TextInput value={tagName} onChange={setTagName}></TextInput>
<Text variant={"heading-md/normal"}>Users (Seperated by comma)</Text>
<TextInput value={userIds} onChange={setUserIDs}></TextInput>
<ExpandableHeader headerText="User List (Click A User To Remove)" defaultState={true}>
{
userIds.split(", ").map(user => {
const userData: any = UserStore.getUser(user);
if (!userData) return null;
return (
<div style={{ display: "flex" }}>
<img src={userData.getAvatarURL()} style={{ height: "20px", borderRadius: "50%", marginRight: "5px" }}></img>
<Text style={{ cursor: "pointer" }} variant={"text-md/normal"} onClick={() => setUserIDs(userIds.replace(`, ${user}`, "").replace(user, ""))}>{userData.globalName || userData.username}</Text>
</div>
);
})
}
</ExpandableHeader>
<Button onClick={async () => {
SavedData = SavedData.filter(data => (data.tagName !== tagName));
await SetData();
update();
}} color={Button.Colors.RED}>Remove</Button>
</>
);
}
function TagConfigurationComponent() {
const update = useForceUpdater();
return (
<>
<Forms.FormDivider />
{
SavedData?.map(e => (
<>
<TagConfigCard tag={e} />
<Forms.FormDivider />
</>
))
}
<Button onClick={() => {
SavedData.push(
{
tagName: "",
userIds: []
});
SetData();
update();
}}>Add</Button>
</>
);
}
const settings = definePluginSettings(
{
tagConfiguration: {
type: OptionType.COMPONENT,
description: "The tag configuration component",
component: () => {
return (
<TagConfigurationComponent />
);
}
}
});
function UserToTagID(user, tag, remove) {
if (remove) {
SavedData.filter(e => e.tagName === tag)[0].userIds = SavedData.filter(e => e.tagName === tag)[0].userIds.filter(e => e !== user);
}
else {
SavedData.filter(e => e.tagName === tag)[0]?.userIds.push(user);
}
SetData();
}
const userPatch: NavContextMenuPatchCallback = (children, { user }) => {
const buttonElement =
<Menu.MenuItem
id="vc-tag-group"
label="Tag"
>
{SavedData.map(tag => {
const isTagged = SavedData.filter(e => e.tagName === tag.tagName)[0].userIds.includes(user.id);
return (
<Menu.MenuItem label={`${isTagged ? "Remove from" : "Add to"} ${tag.tagName}`} key={`vc-tag-${tag.tagName}`} id={`vc-tag-${tag.tagName}`} action={() => { UserToTagID(user.id, tag.tagName, isTagged); }} />
);
})}
</Menu.MenuItem>;
children.push({ ...buttonElement });
};
export default definePlugin({
name: "FriendTags",
description: "Allows you to filter by custom tags in the quick switcher",
authors: [Devs.Samwich],
settings,
queryFriendTags: queryFriendTags,
patches: [
{
find: "#{intl::QUICKSWITCHER_PLACEHOLDER}",
replacement: {
match: /let{selectedIndex:\i,results:\i}/,
replace: "if(this.state.query.includes(\"&\")){ this.props.results = $self.queryFriendTags(this.state.query); }$&"
},
}
],
async start() {
GetData();
},
contextMenus:
{
"user-context": userPatch
}
});

View file

@ -0,0 +1,403 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { ExpandableHeader } from "@components/ExpandableHeader";
import { Heart } from "@components/Heart";
import { Devs } from "@utils/constants";
import { openUserProfile } from "@utils/discord";
import * as Modal from "@utils/modal";
import definePlugin from "@utils/types";
import {
Avatar, Button, ChannelStore,
Clickable, Flex, GuildMemberStore,
GuildStore,
MessageStore,
React,
Text, TextArea, TextInput, Tooltip,
UserStore
} from "@webpack/common";
import { Guild, User } from "discord-types/general";
interface IUserExtra {
isOwner?: boolean;
updatedAt?: number;
}
interface IStorageUser {
id: string;
username: string,
tag: string,
iconURL?: string;
extra?: IUserExtra;
}
interface GroupData {
id: string;
users: { [key: string]: IStorageUser; };
name: string;
}
const constants = {
pluginLabel: "IRememberYou",
pluginId: "irememberyou",
DM: "dm",
DataUIDescription:
"Provides a list of users you have mentioned or replied to, or those who own the servers you belong to (owner*), or are members of your guild",
marks: {
Owner: "owner"
}
};
class Data {
declare usersCollection: Record<string, GroupData>;
declare _storageAutoSaveProtocol_interval;
declare _onMessagePreSend_preSend;
withStart() {
return this;
}
onMessagePreSend(channelId, message, extra) {
const target: Set<{ user: User; source?: Guild, extra: IUserExtra; }> = new Set();
const now = Date.now();
const { replyOptions } = extra;
const guild = (() => {
const channel = ChannelStore.getChannel(channelId);
return GuildStore.getGuild(channel.guild_id) || undefined;
})();
if (replyOptions.messageReference) {
const { channel_id, message_id } = replyOptions.messageReference;
const message = MessageStore.getMessage(channel_id, message_id);
if (!message) {
return;
}
const { author } = message;
target.add({ user: author, source: guild, extra: { updatedAt: now } });
}
if (message.content) {
const { content } = message;
const ids = [...content.matchAll(/<@!?(?<id>\d{17,23})>/g)].map(
({ groups }) => groups.id
);
const users = ids
.map(id => UserStore.getUser(id))
.filter(Boolean);
for (const user of users) {
target.add({ user, source: guild, extra: { updatedAt: now } });
}
}
this.processUsersToCollection([...target]);
}
async processUsersToCollection(
array: { user: User; source?: Guild; extra?: IUserExtra; }[]
) {
const target = this.usersCollection;
for (const { user, source, extra } of array) {
if (user.bot) {
continue;
}
const groupKey = source?.id ?? constants.DM;
const group = (target[groupKey] ||= {
name: source?.name || constants.DM,
id: source?.id || user.id,
users: {}
});
const usersField = group.users;
const previouExtra = usersField[user.id]?.extra ?? {};
const { id, username } = user;
usersField[id] = {
id,
username,
tag: user.discriminator === "0" ? user.username : user.tag,
extra: { ...previouExtra, ...extra },
iconURL: user.getAvatarURL(),
};
}
}
async updateStorage() {
await DataStore.set("irememberyou.data", this.usersCollection);
}
async initializeUsersCollection() {
const data = await DataStore.get("irememberyou.data");
this.usersCollection = data ?? {};
}
writeMembersFromUserGuildsToCollection() {
const target: Set<{ user: User; source?: Guild, extra: IUserExtra; }> =
new Set();
const now = Date.now();
const LIMIT = 1_000;
const clientId = UserStore.getCurrentUser().id;
if (!clientId) {
return;
}
for (const guild of Object.values(GuildStore.getGuilds())) {
const { ownerId } = guild;
if (ownerId !== clientId) {
continue;
}
const members = GuildMemberStore.getMembers(guild.id);
if (members.length > LIMIT) {
members.length = LIMIT;
}
for (const member of members) {
const user = UserStore.getUser(member.userId);
target.add({ user, source: guild, extra: { updatedAt: now } });
}
this.processUsersToCollection([...target]);
}
}
writeGuildsOwnersToCollection() {
const target: Set<{ user: User; source?: Guild; extra: IUserExtra; }> =
new Set();
const now = Date.now();
for (const guild of Object.values(GuildStore.getGuilds())) {
const { ownerId } = guild;
const owner = UserStore.getUser(ownerId);
if (!owner) {
continue;
}
target.add({
user: owner,
source: guild,
extra: { isOwner: true, updatedAt: now },
});
}
this.processUsersToCollection([...target]);
}
storageAutoSaveProtocol() {
this._storageAutoSaveProtocol_interval = setInterval(
this.updateStorage.bind(this),
60_000 * 3
);
}
}
class DataUI {
declare plugin;
constructor(plugin) {
this.plugin = plugin;
}
start() {
return this;
}
renderSectionDescription() {
return <Text>{constants.DataUIDescription}</Text>;
}
renderUsersCollectionAsRows(usersCollection: Data["usersCollection"]) {
if (Object.keys(usersCollection).length === 0) {
return <Text>It's empty right now</Text>;
}
const elements = Object.entries(usersCollection)
.map(([_key, { users, name }]) => ({ name, users: Object.values(users) }))
.sort((a, b) => b.users.length - a.users.length)
.map(({ name, users }) =>
this.renderUsersCollectionRows(name, users)
);
return elements;
}
renderUsersCollectionRows(key: string, users: IStorageUser[]) {
const usersElements = users.map(user => this.renderUserRow(user));
return <aside key={key} >
<ExpandableHeader defaultState={true} headerText={key.toUpperCase()}>
<Flex style={{ gap: "calc(0.5em + 0.5vw) 0.2em", flexDirection: "column" }}>
{usersElements}
</Flex>
</ExpandableHeader>
</aside>;
}
renderUserAvatar(user: IStorageUser) {
return <Clickable onClick={() => openUserProfile(user.id)}>
<span style={{ cursor: "pointer" }} >
<Avatar src={user.iconURL} size="SIZE_24" />
</span>
</Clickable>;
}
userTooltipText(user: IStorageUser) {
const { updatedAt } = user.extra || {};
const updatedAtContent = updatedAt ? new Intl.DateTimeFormat().format(updatedAt) : null;
return `${user.username ?? user.tag}, updated at ${updatedAtContent}`;
}
renderUserRow(user: IStorageUser, allowExtra: { owner?: boolean; } = {}) {
allowExtra = Object.assign({ owner: true }, allowExtra);
return <Flex key={user.id} style={{ margin: 0, width: "100%", flexWrap: "wrap", alignItems: "center" }}>
<span style={{ width: "24em" }}>
<Flex style={{ gap: "0.5em", alignItems: "center", margin: 0, wordBreak: "break-word" }}>
{this.renderUserAvatar(user)}
<Tooltip text={this.userTooltipText(user)}>
{props =>
<Text {...props} selectable>{user.tag} {allowExtra.owner && user.extra?.isOwner && `(${constants.marks.Owner})`}</Text>
}
</Tooltip>
</Flex>
</span>
<span style={{ height: "min-content" }}><Text selectable variant="code" style={{ opacity: 0.75 }}>{user.id}</Text></span>
</Flex>;
}
renderButtonsFooter(usersCollection: Data["usersCollection"]) {
return <footer>
<Flex style={{ gap: "1.5em", marginTop: "2em" }}>
<Clickable onClick={() => Modal.openModal(props => <Modal.ModalRoot size={Modal.ModalSize.LARGE} fullscreenOnMobile={true} {...props}>
<Modal.ModalHeader separator={false}>
<Text
color="header-primary"
variant="heading-lg/semibold"
tag="h1"
style={{ flexGrow: 1 }}
>
Editor
</Text>
<Modal.ModalCloseButton onClick={props.onClose} />
</Modal.ModalHeader>
<Modal.ModalContent>
<Flex style={{ paddingBlock: "0.5em", gap: "0.75em" }}>
<Button label="Validate and save" >Validate and save</Button>
<Button label="Cancel" color={Button.Colors.TRANSPARENT}>Cancel</Button>
</Flex>
<TextArea value={JSON.stringify(usersCollection, null, "\t")} onChange={() => { }} rows={20} />
</Modal.ModalContent>
</Modal.ModalRoot>)}>
<Text variant="eyebrow" style={{ cursor: "pointer" }} >Open editor</Text>
</Clickable>
<Clickable onClick={
async () => {
const confirmed = confirm("Sure?");
if (!confirmed) {
return;
}
const { plugin } = this;
const data = plugin.dataManager as Data;
data.usersCollection = {};
await data.updateStorage();
}
}><Text style={{ cursor: "pointer" }}>Reset storage</Text>
</Clickable>
</Flex>
</footer >;
}
renderSearchElement(usersCollection: Data["usersCollection"]) {
const [current, setState] = React.useState<string>();
const map: Map<string, IStorageUser> = Object.values(usersCollection)
.reduce((acc, { users }) => (acc.push(...Object.values(users)), acc), [] as IStorageUser[])
.reduce((acc, current) => acc.set(current.id, current), new Map());
const list = [...map.values()];
return <section style={{ paddingBlock: "1em" }}>
<TextInput placeholder="Filter by tag, username" name="Filter" onChange={value => setState(value)} />
{current &&
<Flex style={{ flexDirection: "column", gap: "0.5em", paddingTop: "1em" }}>
{list.filter(user => user.tag.includes(current) || user.username.includes(current))
.map(user => this.renderUserRow(
user,
{ owner: false }
))
}
</Flex>
}
</section>;
}
toElement(usersCollection: Data["usersCollection"]) {
return (
/*
> ![Important]
> Let me know a more promising color, instead of #ffffff
*/
<main style={{ color: "#ffffff", paddingBottom: "4em" }}>
<Text tag="h1" variant="heading-lg/bold">
{constants.pluginLabel}{" "}
<Heart />
</Text>
{this.renderSectionDescription()}
<br />
{this.renderSearchElement(usersCollection)}
<Flex style={{ gap: "1.5em", flexDirection: "column" }}>
{this.renderUsersCollectionAsRows(usersCollection)}
</Flex>
{this.renderButtonsFooter(usersCollection)}
</main>
);
}
}
export default definePlugin({
name: "IRememberYou",
description: "Locally saves everyone you've been communicating with (including servers)",
authors: [Devs.zoodogood],
dependencies: ["MessageEventsAPI"],
patches: [],
async start() {
const data = (this.dataManager = await new Data().withStart());
const ui = (this.uiManager = await new DataUI(this).start());
await data.initializeUsersCollection();
data.writeGuildsOwnersToCollection();
data.writeMembersFromUserGuildsToCollection();
data._onMessagePreSend_preSend = addPreSendListener(
data.onMessagePreSend.bind(data)
);
data.storageAutoSaveProtocol();
// @ts-ignore
Vencord.Plugins.plugins.Settings.customSections.push(ID => ({
section: `${constants.pluginId}.display-data`,
label: constants.pluginLabel,
element: () => ui.toElement(data.usersCollection),
}));
},
stop() {
const dataManager = this.dataManager as Data;
removePreSendListener(dataManager._onMessagePreSend_preSend);
clearInterval(dataManager._storageAutoSaveProtocol_interval);
},
});