From 231d3153aea75c8975d302387e3f88dbc76a49fa Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Sat, 11 Jan 2025 01:25:47 +0100 Subject: [PATCH] add types --- src/components/SearchModal.tsx | 178 ++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 83 deletions(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 4f03c19bb..a469e54f7 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -18,8 +18,10 @@ import { } from "@utils/modal"; import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { - Button, ChannelStore, - Flex, GuildStore, + Button, + ChannelStore, + Flex, + GuildStore, Heading, PresenceStore, React, @@ -28,21 +30,21 @@ import { useCallback, useMemo, useRef, - UsernameUtils, UserStore, + UsernameUtils, + UserStore, useState } from "@webpack/common"; -import { Channel, User } from "discord-types/general"; +import { Channel, Guild, User } from "discord-types/general"; const cl = classNameFactory("vc-search-modal-"); // TODO make guilds work -// FIXME fix the no results display +// FIXME fix the no Result display const SearchBarModule = findByPropsLazy("SearchBar", "Checkbox", "AvatarSizes"); const SearchBarWrapper = findByPropsLazy("SearchBar", "Item"); const TextTypes = findByPropsLazy("APPLICATION", "GROUP_DM", "GUILD"); const FrequencyModule = findByPropsLazy("getFrequentlyWithoutFetchingLatest"); -const ConnectionModule = findByPropsLazy("isConnected", "getSocket"); const FrequentsModule = findByPropsLazy("getChannelHistory", "getFrequentGuilds"); const wrapperFn = findByCodeLazy("prevDeps:void 0,"); @@ -56,18 +58,18 @@ const ChannelIcon = findByCodeLazy("channelGuildIcon,"); const GroupDMAvatars = findComponentByCodeLazy("facepileSizeOverride", "recipients.length"); -interface DestinationItemProps { - type: string; +interface DestinationItem { + type: "channel" | "user" | "guild"; id: string; } interface UnspecificRowProps { key: string - destination: DestinationItemProps, + destination: DestinationItem, rowMode: string disabled: boolean, isSelected: boolean, - onPressDestination: (destination: DestinationItemProps) => void, + onPressDestination: (destination: DestinationItem) => void, "aria-posinset": number, "aria-setsize": number } @@ -86,6 +88,32 @@ interface UserIconProps { [key: string]: any; } +interface UserResult { + type: "USER"; + record: User; + score: number; + comparator: string; + sortable?: string; +} + +interface ChannelResult { + type: "TEXT_CHANNEL" | "VOICE_CHANNEL" | "GROUP_DM"; + record: Channel; + score: number; + comparator: string; + sortable?: string; +} + +interface GuildResult { + type: "GUILD"; + record: Guild; + score: number; + comparator: string; + sortable?: string; +} + +type Result = UserResult | ChannelResult | GuildResult; + const searchTypesToResultTypes = (type: string | string[]) => { if (type === "ALL") return ["USER", "TEXT_CHANNEL", "VOICE_CHANNEL", "GROUP_DM", "GUILD"]; if (typeof type === "string") { @@ -113,9 +141,20 @@ function searchTypeToText(type: string | string[]) { } } +/** + * SearchModal component for displaying a modal with search functionality, built after Discord's forwarding Modal. + * + * @param {Object} props - The props for the SearchModal component. + * @param {ModalProps} props.modalProps - The modal props. + * @param {function} props.onSubmit - The function to call when the user submits their selection. + * @param {string} [props.input] - The initial input value for the search bar. + * @param {("USERS" | "CHANNELS" | "GUILDS")[] | "USERS" | "CHANNELS" | "GUILDS" | "ALL"} [props.searchType="ALL"] - The type of items to search for. + * @param {string} [props.subText] - Additional text to display below the heading. + * @returns The rendered SearchModal component. + */ export default function SearchModal({ modalProps, onSubmit, input, searchType = "ALL", subText }: { modalProps: ModalProps; - onSubmit(selected: DestinationItemProps[]): void; + onSubmit(selected: DestinationItem[]): void; input?: string; searchType?: ("USERS" | "CHANNELS" | "GUILDS")[] | "USERS" | "CHANNELS" | "GUILDS" | "ALL"; subText?: string @@ -123,7 +162,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const callbacks = new Map(); - function registerCallback(key, callback) { + function registerCallback(key: string, callback: (...args: any[]) => void): () => void { let currentCallbacks = callbacks.get(key); if (!currentCallbacks) { currentCallbacks = new Set(); @@ -164,14 +203,13 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const resultTypes = searchTypesToResultTypes(searchType); - const [selected, setSelected] = useState([]); + const [selected, setSelected] = useState([]); const refCounter = useRef(0); const rowContext = React.createContext({ id: "NO_LIST", - setFocus(id: string) { - } + setFocus(id: string) {} }); const Row = (props: SpecificRowProps) => { @@ -196,7 +234,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = return ( { handlePress(); e.preventDefault(); e.stopPropagation(); }} + onClick={e => { e.stopPropagation(); e.preventDefault(); handlePress(); }} aria-selected={isSelected} {...interactionProps} {...rest} @@ -248,7 +286,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = ); } - function generateChannelLabel(channel: Channel) { + function generateChannelLabel(channel: Channel): string { return getChannelLabel(channel, UserStore, RelationshipStore, false); } @@ -257,7 +295,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const channelLabel = generateChannelLabel(channel); - const parentChannelLabel = () => { + const parentChannelLabel = (): string => { const parentChannel = ChannelStore.getChannel(channel.parent_id); return parentChannel ? getChannelLabel(parentChannel, UserStore, RelationshipStore, false) : null; }; @@ -310,7 +348,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = .map(user => UsernameUtils.getName(user)); if (!userNames || userNames.length === 0 || channel.name === "") - return null; + return ""; if (userNames.length <= 3) return userNames.join(", "); const amount = userNames.length - 3; @@ -325,7 +363,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = icon={} label={label} - subLabel={subLabelValue ?? ""} + subLabel={subLabelValue} /> ); } @@ -350,7 +388,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = }; } - function navigatorData(e) { + function navigatorData(e: { children: (data: ReturnType) => React.ReactNode }): React.ReactNode { const { children } = e; return children(generateNavigatorData()); } @@ -364,7 +402,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const handleFocus = useCallback(() => setFocus(rowId), [rowId, setFocus]); React.useLayoutEffect(() => { - return registerCallback(id, (tabIndex, id) => { + return registerCallback(id, (tabIndex: string, id: string) => { setTabIndex(id && tabIndex === rowId ? 0 : -1); }); }, [rowId, id]); @@ -380,22 +418,21 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const [searchText, setSearchText] = useState(input || ""); const ref = {}; - function getSearchHandler(e) { - const { searchOptions } = e; - const [results, setResults] = useState({ + function getSearchHandler(searchOptions: Record): { search: (e: { query: string, resultTypes: string[] }) => void, results: Result[], query: string } { + const [results, setResults] = useState<{ results: Result[], query: string }>({ results: [], query: "" }); - function getRef(e) { // FIXME probably should use a proper type for this - const ref_ = useRef(ref); + function getRef(e: () => T): T { + const ref_ = useRef(ref as T); if (ref_.current === ref) ref_.current = e(); return ref_.current; } - const searchHandler: typeof SearchHandler = getRef(() => { - const searchHandler = new SearchHandler((r, q) => { + const searchHandler: InstanceType = getRef(() => { + const searchHandler = new SearchHandler((r: Result[], q: string) => { setResults({ results: r, query: q @@ -426,7 +463,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = }; } - function generateResults({ selectedDestinations }) { + function generateResults({ selectedDestinations }: { selectedDestinations: DestinationItem[] }) { const { search, query, results } = getSearchHandler({ blacklist: null, frecencyBoosters: !0, @@ -454,10 +491,9 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = loadFunction(); const frequentChannels = wrapperFn([FrequencyModule], () => FrequencyModule.getFrequentlyWithoutFetchingLatest()); - const isConnected = wrapperFn([ConnectionModule], () => ConnectionModule.isConnected()); const hasQuery = query !== ""; - function getItem(e) { + function getItem(e: DestinationItem): Result { if (e.type !== "user") return convertItem(e.id); { @@ -472,68 +508,45 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = } } - function processItems(items, existingItems?) { - let temp: null; - const set = new Set(existingItems || []); - - const array: any[] = []; - - items.forEach(item => { - if (item != null) { - if (item.type === TextTypes.HEADER) temp = item; - else { - const { id } = item.record; - if (!set.has(id)) { - set.add(item); - if (temp != null) { - array.push(temp); - temp = null; - } - array.push(item); - } - } - } - }); - return array; - } - const filterItems = (items: any[]) => { return items.filter( - item => item != null && (item.type === TextTypes.HEADER || resultTypes.includes(item.type)) + item => item != null && resultTypes.includes(item.type) ); }; - function filterResults(e) { - const removeDuplicates = (arr: any[]) => { + function filterResults(props: { + results: Result[]; + hasQuery: boolean; + frequentChannels: Channel[]; + pinnedDestinations: DestinationItem[]; + }): Result[] { + const removeDuplicates = (arr: Result[]): Result[] => { const clean: any[] = []; const seenIds = new Set(); arr.forEach(item => { if (item == null || item.record == null) return; - const id = item.type === "user" ? item.id : item.record.id; - if (!seenIds.has(id)) { - seenIds.add(id); + if (!seenIds.has(item.record.id)) { + seenIds.add(item.record.id); clean.push(item); } }); return clean; }; - const { results, hasQuery, frequentChannels, pinnedDestinations } = e; - if (hasQuery) return processItems(filterItems(results)); + const { results, hasQuery, frequentChannels, pinnedDestinations } = props; + if (hasQuery) return filterItems(results); - const channelHistory = FrequentsModule.getChannelHistory(); + const channelHistory: string[] = FrequentsModule.getChannelHistory(); const recentDestinations = filterItems([ ...(channelHistory.length > 0 ? channelHistory.map(e => convertItem(e)) : []), ...(frequentChannels.length > 0 ? frequentChannels.map(e => convertItem(e.id)) : []) ]); - const destinations = removeDuplicates( + return removeDuplicates( [...(pinnedDestinations.length > 0 ? pinnedDestinations.map(e => getItem(e)) : []), ...recentDestinations ]); - - return processItems(destinations); } return { @@ -542,8 +555,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = hasQuery: hasQuery, frequentChannels: frequentChannels, pinnedDestinations: pinned, - isConnected: isConnected - }), [results, hasQuery, frequentChannels, pinned, isConnected]), + }), [results, hasQuery, frequentChannels, pinned]), updateSearchText: updateSearch }; } @@ -558,12 +570,11 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const rowHeight = useCallback(() => 48, []); - function ModalScroller(e) { + function ModalScroller({ rowData, handleToggleDestination, paddingBottom, paddingTop }: { rowData: Result[], handleToggleDestination: (destination: DestinationItem) => void, paddingBottom?: number, paddingTop?: number }) { - const { rowData: t, handleToggleDestination, ...extraProps } = e; - const sectionCount = useMemo(() => [t.length], [t.length]); + const sectionCount: number[] = useMemo(() => [rowData.length], [rowData.length]); - const callback = useCallback(e => { + const callback = useCallback((e: { section: number, row: number }) => { const { section, row } = e; if (section > 0) return; @@ -571,8 +582,8 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = if (type === TextTypes.HEADER) return; - const destination = { - type: type === TextTypes.USER ? "user" : "channel", + const destination: DestinationItem = { + type: type === TextTypes.USER ? "user" : type === TextTypes.GUILD ? "guild" : "channel", id: record.id }; @@ -590,11 +601,11 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = "aria-setsize": results.length }; - if (type === TextTypes.USER) + if (type === "USER") return generateUserItem(record, rowProps); - if (type === TextTypes.GROUP_DM) + if (type === "GROUP_DM") return generateGdmItem(record, rowProps); - if (type === TextTypes.TEXT_CHANNEL || type === TextTypes.VOICE_CHANNEL) { + if (type === "TEXT_CHANNEL" || type === "VOICE_CHANNEL") { return generateChannelItem(record, rowProps); } else throw new Error("Unknown type " + type); }, [results, selectedDestinationKeys, handleToggleDestination]); @@ -614,7 +625,8 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = } } {...data} - {...extraProps} + paddingBottom={paddingBottom} + paddingTop={paddingTop} sections={sectionCount} sectionHeight={0} renderRow={callback} @@ -625,8 +637,8 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = } - const setSelectedCallback = useCallback(e => { - setSelected(currentSelected => { + const setSelectedCallback = useCallback((e: DestinationItem) => { + setSelected((currentSelected: DestinationItem[]) => { const index = currentSelected.findIndex(item => { const { type, id } = item; return type === e.type && id === e.id;