diff --git a/src/equicordplugins/iRememberYou/components/data.tsx b/src/equicordplugins/iRememberYou/components/data.tsx new file mode 100644 index 000000000..a00e09630 --- /dev/null +++ b/src/equicordplugins/iRememberYou/components/data.tsx @@ -0,0 +1,172 @@ +/* + * 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 { Guild, User } from "@vencord/discord-types"; +import { ChannelStore, GuildMemberStore, GuildStore, MessageStore, UserStore, } from "@webpack/common"; + +export interface IUserExtra { + isOwner?: boolean; + updatedAt?: number; +} + +export interface IStorageUser { + id: string; + username: string, + tag: string, + iconURL?: string; + extra?: IUserExtra; +} + +export interface GroupData { + id: string; + users: { [key: string]: IStorageUser; }; + name: string; +} + +export class Data { + declare usersCollection: Record; + 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(/<@!?(?\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 ?? "dm"; + const group = (target[groupKey] ||= { + name: source?.name || "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 + ); + } +} diff --git a/src/equicordplugins/iRememberYou/components/ui.tsx b/src/equicordplugins/iRememberYou/components/ui.tsx new file mode 100644 index 000000000..eff60ab0a --- /dev/null +++ b/src/equicordplugins/iRememberYou/components/ui.tsx @@ -0,0 +1,119 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { BaseText } from "@components/BaseText"; +import { Paragraph } from "@components/Paragraph"; +import { openUserProfile } from "@utils/discord"; +import { Avatar, Clickable, Flex, React, TextInput, Tooltip, } from "@webpack/common"; + +import { Data, IStorageUser } from "./data"; + +const cl = classNameFactory("vc-i-remember-you-"); + +function tooltipText(user: IStorageUser) { + const { updatedAt } = user.extra || {}; + const updatedAtContent = updatedAt ? new Intl.DateTimeFormat().format(updatedAt) : null; + return `${user.username ?? user.tag}, updated at ${updatedAtContent}`; +} + +function SectionDescription() { + return ( + + {"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"} + + ); +} + +function UsersCollectionRows({ usersCollection }: { usersCollection: Data["usersCollection"]; }) { + if (Object.keys(usersCollection).length === 0) return ( + + It's empty right now + + ); + + return ( + <> + {Object.entries(usersCollection) + .map(([_key, { users, name }]) => ({ name, users: Object.values(users) })) + .sort((a, b) => b.users.length - a.users.length) + .map(({ name, users }) => ( + + ))} + + ); +} + +function UserRow({ user, allowOwner = true }: { user: IStorageUser, allowOwner?: boolean; }) { + return ( + + + + openUserProfile(user.id)}> + + + + + + {props => + + {user.tag} {allowOwner && user.extra?.isOwner && "(owner)"} + + } + + + + + + + {user.id} + + + + ); +} + +function SearchElement({ usersCollection }: { usersCollection: Data["usersCollection"]; }) { + const [current, setCurrent] = React.useState(""); + const list = Object.values(usersCollection).flatMap(col => Object.values(col.users)) as IStorageUser[]; + + return ( +
+ + {current && ( + + {list.filter(user => user.tag.includes(current) || user.username.includes(current)) + .map(user => )} + + )} +
+ ); +} + +export function DataUI({ plugin, usersCollection }: { plugin: any; usersCollection: Data["usersCollection"]; }) { + return ( +
+ + IRememberYou + + + + + + + +
+ ); +} diff --git a/src/equicordplugins/iRememberYou/index.tsx b/src/equicordplugins/iRememberYou/index.tsx index a17255c88..516a3fdfa 100644 --- a/src/equicordplugins/iRememberYou/index.tsx +++ b/src/equicordplugins/iRememberYou/index.tsx @@ -6,362 +6,13 @@ import "./styles.css"; -import { DataStore } from "@api/index"; import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; -import { classNameFactory } from "@api/Styles"; -import { BaseText } from "@components/BaseText"; -import { Paragraph } from "@components/Paragraph"; import { EquicordDevs } from "@utils/constants"; -import { openUserProfile } from "@utils/discord"; -import * as Modal from "@utils/modal"; import definePlugin from "@utils/types"; -import { Guild, User } from "@vencord/discord-types"; -import { Avatar, Button, ChannelStore, Clickable, Flex, GuildMemberStore, GuildStore, MessageStore, React, TextArea, TextInput, Tooltip, UserStore, } from "@webpack/common"; +import { React, } from "@webpack/common"; -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 cl = classNameFactory("vc-i-remember-you-"); - -class Data { - declare usersCollection: Record; - 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(/<@!?(?\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 ?? "dm"; - const group = (target[groupKey] ||= { - name: source?.name || "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 {"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"}; - } - - renderUsersCollectionAsRows(usersCollection: Data["usersCollection"]) { - if (Object.keys(usersCollection).length === 0) { - return It's empty right now; - } - 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 ; - } - - renderUserAvatar(user: IStorageUser) { - return openUserProfile(user.id)}> - - - - ; - } - - 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 ( - - - - {this.renderUserAvatar(user)} - - {props => - - {user.tag} {allowExtra.owner && user.extra?.isOwner && "(owner)"} - - } - - - - - - - {user.id} - - - - ); - } - - renderButtonsFooter(usersCollection: Data["usersCollection"]) { - return ( -