diff --git a/cspell.json b/cspell.json index 7414a8898..a4cfadff1 100644 --- a/cspell.json +++ b/cspell.json @@ -19,6 +19,7 @@ "coremods", "crosspost", "customitem", + "dataurl", "dependants", "devmode", "discordapp", @@ -29,6 +30,7 @@ "flatpaks", "fontawesome", "Fonticons", + "Frecency", "getent", "gifv", "globstar", diff --git a/scripts/build.mts b/scripts/build.mts index a879f74c0..799306d33 100644 --- a/scripts/build.mts +++ b/scripts/build.mts @@ -102,6 +102,9 @@ const contexts = await Promise.all([ target: `chrome${CHROME_VERSION}`, outfile: `${distDir}/renderer.js`, format: "esm", + loader: { + ".png": "dataurl", + }, }), ]); await Promise.all( diff --git a/src/renderer/apis/commands.ts b/src/renderer/apis/commands.ts index 81e04edaa..509ee507c 100644 --- a/src/renderer/apis/commands.ts +++ b/src/renderer/apis/commands.ts @@ -16,7 +16,7 @@ import { constants, i18n, messages, users } from "../modules/common"; import type { Store } from "../modules/common/flux"; import { Logger } from "../modules/logger"; import { filters, getByStoreName, waitForModule } from "../modules/webpack"; - +import icon from "../assets/logo.png"; const logger = Logger.api("Commands"); let RepluggedUser: User | undefined; @@ -42,7 +42,7 @@ export const defaultSection: RepluggedCommandSection = Object.freeze({ id: "replugged", name: "Replugged", type: 1, - icon: "https://cdn.discordapp.com/attachments/1000955992068079716/1004196106055454820/Replugged-Logo.png", + icon, }); export class CommandInteraction { diff --git a/src/renderer/assets/assets.d.ts b/src/renderer/assets/assets.d.ts new file mode 100644 index 000000000..a11558e67 --- /dev/null +++ b/src/renderer/assets/assets.d.ts @@ -0,0 +1,3 @@ +declare module "*.png" { + export default content as string; +} diff --git a/src/renderer/assets/logo.png b/src/renderer/assets/logo.png new file mode 100644 index 000000000..a9d99c1c6 Binary files /dev/null and b/src/renderer/assets/logo.png differ diff --git a/src/renderer/coremods/commands/index.ts b/src/renderer/coremods/commands/index.ts index 4836625d0..255e9ba23 100644 --- a/src/renderer/coremods/commands/index.ts +++ b/src/renderer/coremods/commands/index.ts @@ -1,4 +1,6 @@ import type { AnyRepluggedCommand, RepluggedCommandSection } from "../../../types"; +import type { Channel, Guild } from "discord-types/general"; +import type { Store } from "@common/flux"; import { Injector } from "../../modules/injector"; import { Logger } from "../../modules/logger"; import { waitForProps } from "../../modules/webpack"; @@ -9,35 +11,79 @@ import { loadCommands, unloadCommands } from "./commands"; const logger = Logger.api("Commands"); const injector = new Injector(); -interface ApplicationCommandSearchStoreMod { - useDiscoveryState: (...args: unknown[]) => +type CommandState = + | { + fetchState: { fetching: boolean }; + result: { + sectionIdsByBotId: Record; + sections: Record< + string, + { commands: Record; descriptor: RepluggedCommandSection } + >; + version: string; + }; + serverVersion: symbol | string; + } + | undefined; + +interface ApplicationCommandIndexStore extends Store { + getContextState: (channel: Channel) => CommandState; + getUserState: () => CommandState; + query: ( + channel: Channel, + queryOptions: { + commandType: number; + text: string; + }, + fetchOptions: { + allowFetch: boolean; + limit: number; + includeFrecency?: boolean; + placeholderCount?: number; + scoreMethod?: string; + }, + ) => | { - sectionDescriptors: RepluggedCommandSection[]; + descriptors: RepluggedCommandSection[]; commands: AnyRepluggedCommand[]; - filteredSectionId: string | null; - activeSections: RepluggedCommandSection[]; - commandsByActiveSection: Array<{ - section: RepluggedCommandSection; - data: AnyRepluggedCommand[]; - }>; + sectionedCommands: Array<{ data: AnyRepluggedCommand[]; section: RepluggedCommandSection }>; + loading: boolean; } | undefined; - useQueryState: (...args: unknown[]) => unknown; - useSearchStoreOpenState: (...args: unknown[]) => unknown; - search: (...args: unknown[]) => unknown; - default: ApplicationCommandSearchStore; } -interface ApplicationCommandSearchStore { - getChannelState: (...args: unknown[]) => +interface ApplicationCommandIndexStoreMod { + useContextIndexState: ( + channel: Channel, + allowCache: boolean, + allowFetch: boolean, + ) => CommandState; + useDiscoveryState: ( + channel: Channel, + guild: Guild, + commandOptions: { + commandType: number; + applicationCommands?: boolean; + builtIns?: "allow" | "deny"; + }, + fetchOptions: { + allowFetch: boolean; + limit: number; + includeFrecency?: boolean; + placeholderCount?: number; + scoreMethod?: string; + }, + ) => | { - applicationSections: RepluggedCommandSection[]; - applicationCommands: AnyRepluggedCommand[]; + descriptors: RepluggedCommandSection[]; + commands: AnyRepluggedCommand[]; + loading: boolean; + sectionedCommands: Array<{ data: AnyRepluggedCommand[]; section: RepluggedCommandSection }>; } | undefined; - getApplicationSections: (...args: unknown[]) => RepluggedCommandSection[] | undefined; - useSearchManager: (...args: unknown[]) => unknown; - getQueryCommands: (...args: [string, string, string]) => AnyRepluggedCommand[] | undefined; + useGuildIndexState: (guildId: string, allowFetch: boolean) => CommandState; + useUserIndexState: (allowCache: boolean, allowFetch: boolean) => CommandState; + default: ApplicationCommandIndexStore; } async function injectRepluggedBotIcon(): Promise { @@ -64,197 +110,159 @@ async function injectRepluggedSectionIcon(): Promise { ); } -async function injectApplicationCommandSearchStore(): Promise { +async function injectApplicationCommandIndexStore(): Promise { // The module which contains the store - const ApplicationCommandSearchStoreMod = await waitForProps( + const ApplicationCommandIndexStoreMod = await waitForProps( + "useContextIndexState", "useDiscoveryState", - "useQueryState", - "useSearchStoreOpenState", - "search", + "useGuildIndexState", + "useUserIndexState", ); - // Base handler function for ApplicationCommandSearchStore which is ran to get the info in store + // Base handler function for ApplicationCommandIndexStore which is ran to get the info in store // commands are mainly added here - injector.after(ApplicationCommandSearchStoreMod, "useDiscoveryState", (_, res) => { - const commandAndSectionsArray = Array.from(commandAndSections.values()).filter( - (commandAndSection) => commandAndSection.commands.size, - ); - if (!res || !commandAndSectionsArray.length) return res; - if ( - !Array.isArray(res.sectionDescriptors) || - !commandAndSectionsArray.every((commandAndSection) => - res.sectionDescriptors.some((section) => section.id === commandAndSection.section.id), - ) - ) { - const sectionsToAdd = commandAndSectionsArray - .map((commandAndSection) => commandAndSection.section) - .filter((section) => !res.sectionDescriptors.includes(section)); - if (res.sectionDescriptors.some((section) => section.id === "-2")) { - res.sectionDescriptors.splice(1, 0, ...sectionsToAdd); - } else { - res.sectionDescriptors = Array.isArray(res.sectionDescriptors) - ? [...sectionsToAdd, ...res.sectionDescriptors] - : sectionsToAdd; + injector.after( + ApplicationCommandIndexStoreMod, + "useDiscoveryState", + ([, , { commandType }], res) => { + const commandAndSectionsArray = Array.from(commandAndSections.values()).filter( + (commandAndSection) => commandAndSection.commands.size, + ); + if (!res || !commandAndSectionsArray.length || commandType !== 1) return res; + if ( + !Array.isArray(res.descriptors) || + !commandAndSectionsArray.every((commandAndSection) => + res.descriptors.some((section) => section.id === commandAndSection.section.id), + ) + ) { + const sectionsToAdd = commandAndSectionsArray + .map((commandAndSection) => commandAndSection.section) + .filter((section) => !res.descriptors.includes(section)); + if (res.descriptors.some((section) => section.id === "-2")) { + res.descriptors.splice(1, 0, ...sectionsToAdd); + } else { + res.descriptors = Array.isArray(res.descriptors) + ? [...sectionsToAdd, ...res.descriptors] + : sectionsToAdd; + } } - } - if ( - res.filteredSectionId === null || - commandAndSectionsArray.some( - (commandAndSection) => res.filteredSectionId === commandAndSection.section.id, - ) - ) { - const sectionsToAdd = commandAndSectionsArray - .map((commandAndSection) => commandAndSection.section) - .filter( - (section) => - (res.filteredSectionId == null || res.filteredSectionId === section.id) && - !res.activeSections.includes(section), + if ( + !Array.isArray(res.commands) || + commandAndSectionsArray.some((commandAndSection) => + Array.from(commandAndSection.commands.values()).some( + (command) => !res.commands.includes(command), + ), + ) + ) { + const commandsToAdd = commandAndSectionsArray + .map((commandAndSection) => Array.from(commandAndSection.commands.values())) + .flat(10); + const indexAt = res.commands.findIndex( + (c) => + c.id === res.sectionedCommands.find(({ section }) => section.id === "-2")?.data[0].id, ); - if (res.activeSections.some((section) => section.id === "-2")) { - res.activeSections.splice(1, 0, ...sectionsToAdd); - } else { - res.activeSections = Array.isArray(res.activeSections) - ? [...sectionsToAdd, ...res.activeSections] - : sectionsToAdd; + if (indexAt) { + res.commands.splice(indexAt, 0, ...commandsToAdd); + } else { + res.commands = Array.isArray(res.commands) + ? [ + ...commandsToAdd, + ...res.commands.filter((command) => !commandsToAdd.includes(command)), + ] + : commandsToAdd; + } } - const commandsBySectionToAdd = commandAndSectionsArray - .filter( - (commandAndSection) => - (res.filteredSectionId !== null - ? res.filteredSectionId === commandAndSection.section.id - : true) && - !res.commandsByActiveSection.some( - (activeCommandAndSection) => - activeCommandAndSection.section.id === commandAndSection.section.id, - ), + if ( + !Array.isArray(res.sectionedCommands) || + !commandAndSectionsArray.every((commandAndSection) => + res.sectionedCommands.some(({ section }) => section.id === commandAndSection.section.id), ) - .map((commandAndSection) => ({ + ) { + const dataToAdd = commandAndSectionsArray.map((commandAndSection) => ({ section: commandAndSection.section, data: Array.from(commandAndSection.commands.values()), })); - - if ( - res.commandsByActiveSection.some( - (activeCommandAndSections) => activeCommandAndSections.section.id === "-2", - ) - ) { - res.commandsByActiveSection.splice(1, 0, ...commandsBySectionToAdd); - } else { - res.commandsByActiveSection = Array.isArray(res.commandsByActiveSection) - ? [...commandsBySectionToAdd, ...res.commandsByActiveSection] - : commandsBySectionToAdd; + if (res.sectionedCommands.some(({ section }) => section.id === "-2")) { + res.sectionedCommands.splice(1, 0, ...dataToAdd); + } else { + res.sectionedCommands = Array.isArray(res.sectionedCommands) + ? [...dataToAdd, ...res.sectionedCommands] + : dataToAdd; + } } - } - if ( - !Array.isArray(res.commands) || - commandAndSectionsArray.some((commandAndSection) => - Array.from(commandAndSection.commands.values()).some( - (command) => !res.commands.includes(command), - ), - ) - ) { - const commandsToAdd = commandAndSectionsArray - .map((commandAndSection) => Array.from(commandAndSection.commands.values())) - .flat(10); - res.commands = Array.isArray(res.commands) - ? [...res.commands.filter((command) => !commandsToAdd.includes(command)), ...commandsToAdd] - : commandsToAdd; - } - return res; - }); + return res; + }, + ); // The store itself - const ApplicationCommandSearchStore = ApplicationCommandSearchStoreMod.default; + const ApplicationCommandIndexStore = ApplicationCommandIndexStoreMod.default; - // Channel state gets update with each character entered in text box and search so we patch this to keep our custom section - // even after updates happen - injector.after(ApplicationCommandSearchStore, "getChannelState", (_, res) => { - const commandAndSectionsArray = Array.from(commandAndSections.values()).filter( - (commandAndSection) => commandAndSection.commands.size, - ); - if (!res || !commandAndSectionsArray.length) return res; - if ( - !Array.isArray(res.applicationSections) || - !commandAndSectionsArray.every((commandAndSection) => - res.applicationSections.some((section) => section.id === commandAndSection.section.id), - ) - ) { - const sectionsToAdd = commandAndSectionsArray.map( - (commandAndSection) => commandAndSection.section, - ); - res.applicationSections = Array.isArray(res.applicationSections) - ? [...sectionsToAdd, ...res.applicationSections] - : sectionsToAdd; - } - if ( - !Array.isArray(res.applicationCommands) || - commandAndSectionsArray.some((commandAndSection) => - Array.from(commandAndSection.commands.values()).some( - (command) => !res.applicationCommands.includes(command), - ), - ) - ) { - const commandsToAdd = commandAndSectionsArray - .map((commandAndSection) => Array.from(commandAndSection.commands.values())) - .flat(10); - res.applicationCommands = Array.isArray(res.applicationCommands) - ? [ - ...commandsToAdd, - ...res.applicationCommands.filter((command) => !commandsToAdd.includes(command)), - ] - : commandsToAdd; - } - return res; - }); - - // Makes sure if our custom section is included or not - // Add it if not - injector.after(ApplicationCommandSearchStore, "getApplicationSections", (_, res) => { - res ??= []; - const commandAndSectionsArray = Array.from(commandAndSections.values()).filter( - (commandAndSection) => commandAndSection.commands.size, - ); - if (!commandAndSectionsArray.length) return; - if ( - !commandAndSectionsArray.every( - (commandAndSection) => res?.some((section) => section.id === commandAndSection.section.id), - ) - ) { - const sectionsToAdd = commandAndSectionsArray - .map((commandAndSection) => commandAndSection.section) - .filter((section) => res?.some((existingSections) => section.id === existingSections.id)); - res.push(...sectionsToAdd); - } - return res; - }); - - // Slash command search patched to return our slash commands too + // Slash command indexing patched to return our slash commands too // only those which match tho - injector.after(ApplicationCommandSearchStore, "getQueryCommands", ([_, __, query], res) => { - if (!query || query.startsWith("/")) return res; + injector.after( + ApplicationCommandIndexStore, + "query", + ([_, { text: query }]: [unknown, { text?: string }], res) => { + if (!query || query.startsWith("/")) return res; - res ??= []; - const commandsToAdd = Array.from(commandAndSections.values()) - .filter((commandAndSection) => commandAndSection.commands.size) - .map((commandAndSection) => Array.from(commandAndSection.commands.values())) - .flat(10); - for (const command of commandsToAdd) { - const exists = res.some((c) => c.id === command.id); + const commandAndSectionsArray = Array.from(commandAndSections.values()) + .map((commandAndSection) => ({ + section: commandAndSection.section, + commands: Array.from(commandAndSection.commands.values()).filter((c) => + c.name.includes(query), + ), + })) + .filter((commandAndSection) => commandAndSection.commands.length); + if (!res || !commandAndSectionsArray.length) return res; - if (exists || !command.name.includes(query)) { - continue; + if ( + !Array.isArray(res.descriptors) || + !commandAndSectionsArray.every((commandAndSection) => + res.descriptors.some((section) => section.id === commandAndSection.section.id), + ) + ) { + const sectionsToAdd = commandAndSectionsArray.map( + (commandAndSection) => commandAndSection.section, + ); + res.descriptors = Array.isArray(res.commands) + ? [...sectionsToAdd, ...res.descriptors] + : sectionsToAdd; } - - try { - res.unshift(command); - } catch { - res = [command, ...res]; + if ( + !Array.isArray(res.commands) || + commandAndSectionsArray.some((commandAndSection) => + Array.from(commandAndSection.commands).some((command) => !res.commands.includes(command)), + ) + ) { + const commandsToAdd = commandAndSectionsArray + .map((commandAndSection) => commandAndSection.commands) + .flat(10); + res.commands = Array.isArray(res.commands) + ? [ + ...commandsToAdd, + ...res.commands.filter((command) => !commandsToAdd.includes(command)), + ] + : commandsToAdd; } - } - return res; - }); + if ( + !Array.isArray(res.sectionedCommands) || + !commandAndSectionsArray.every((commandAndSection) => + res.sectionedCommands.some(({ section }) => section.id === commandAndSection.section.id), + ) + ) { + const dataToAdd = commandAndSectionsArray.map((commandAndSection) => ({ + section: commandAndSection.section, + data: commandAndSection.commands, + })); + res.sectionedCommands = Array.isArray(res.sectionedCommands) + ? [...dataToAdd, ...res.sectionedCommands] + : dataToAdd; + } + return res; + }, + ); } async function injectProfileFetch(): Promise { @@ -271,7 +279,7 @@ async function injectProfileFetch(): Promise { export async function start(): Promise { await injectRepluggedBotIcon(); await injectRepluggedSectionIcon(); - await injectApplicationCommandSearchStore(); + await injectApplicationCommandIndexStore(); await injectProfileFetch(); loadCommands(); }