add types

This commit is contained in:
Elvyra 2025-01-11 01:25:47 +01:00
parent ebd6a2b27b
commit 231d3153ae

View file

@ -18,8 +18,10 @@ import {
} from "@utils/modal"; } from "@utils/modal";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { import {
Button, ChannelStore, Button,
Flex, GuildStore, ChannelStore,
Flex,
GuildStore,
Heading, Heading,
PresenceStore, PresenceStore,
React, React,
@ -28,21 +30,21 @@ import {
useCallback, useCallback,
useMemo, useMemo,
useRef, useRef,
UsernameUtils, UserStore, UsernameUtils,
UserStore,
useState useState
} from "@webpack/common"; } from "@webpack/common";
import { Channel, User } from "discord-types/general"; import { Channel, Guild, User } from "discord-types/general";
const cl = classNameFactory("vc-search-modal-"); const cl = classNameFactory("vc-search-modal-");
// TODO make guilds work // TODO make guilds work
// FIXME fix the no results display // FIXME fix the no Result display
const SearchBarModule = findByPropsLazy("SearchBar", "Checkbox", "AvatarSizes"); const SearchBarModule = findByPropsLazy("SearchBar", "Checkbox", "AvatarSizes");
const SearchBarWrapper = findByPropsLazy("SearchBar", "Item"); const SearchBarWrapper = findByPropsLazy("SearchBar", "Item");
const TextTypes = findByPropsLazy("APPLICATION", "GROUP_DM", "GUILD"); const TextTypes = findByPropsLazy("APPLICATION", "GROUP_DM", "GUILD");
const FrequencyModule = findByPropsLazy("getFrequentlyWithoutFetchingLatest"); const FrequencyModule = findByPropsLazy("getFrequentlyWithoutFetchingLatest");
const ConnectionModule = findByPropsLazy("isConnected", "getSocket");
const FrequentsModule = findByPropsLazy("getChannelHistory", "getFrequentGuilds"); const FrequentsModule = findByPropsLazy("getChannelHistory", "getFrequentGuilds");
const wrapperFn = findByCodeLazy("prevDeps:void 0,"); const wrapperFn = findByCodeLazy("prevDeps:void 0,");
@ -56,18 +58,18 @@ const ChannelIcon = findByCodeLazy("channelGuildIcon,");
const GroupDMAvatars = findComponentByCodeLazy("facepileSizeOverride", "recipients.length"); const GroupDMAvatars = findComponentByCodeLazy("facepileSizeOverride", "recipients.length");
interface DestinationItemProps { interface DestinationItem {
type: string; type: "channel" | "user" | "guild";
id: string; id: string;
} }
interface UnspecificRowProps { interface UnspecificRowProps {
key: string key: string
destination: DestinationItemProps, destination: DestinationItem,
rowMode: string rowMode: string
disabled: boolean, disabled: boolean,
isSelected: boolean, isSelected: boolean,
onPressDestination: (destination: DestinationItemProps) => void, onPressDestination: (destination: DestinationItem) => void,
"aria-posinset": number, "aria-posinset": number,
"aria-setsize": number "aria-setsize": number
} }
@ -86,6 +88,32 @@ interface UserIconProps {
[key: string]: any; [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[]) => { const searchTypesToResultTypes = (type: string | string[]) => {
if (type === "ALL") return ["USER", "TEXT_CHANNEL", "VOICE_CHANNEL", "GROUP_DM", "GUILD"]; if (type === "ALL") return ["USER", "TEXT_CHANNEL", "VOICE_CHANNEL", "GROUP_DM", "GUILD"];
if (typeof type === "string") { 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 }: { export default function SearchModal({ modalProps, onSubmit, input, searchType = "ALL", subText }: {
modalProps: ModalProps; modalProps: ModalProps;
onSubmit(selected: DestinationItemProps[]): void; onSubmit(selected: DestinationItem[]): void;
input?: string; input?: string;
searchType?: ("USERS" | "CHANNELS" | "GUILDS")[] | "USERS" | "CHANNELS" | "GUILDS" | "ALL"; searchType?: ("USERS" | "CHANNELS" | "GUILDS")[] | "USERS" | "CHANNELS" | "GUILDS" | "ALL";
subText?: string subText?: string
@ -123,7 +162,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
const callbacks = new Map(); const callbacks = new Map();
function registerCallback(key, callback) { function registerCallback(key: string, callback: (...args: any[]) => void): () => void {
let currentCallbacks = callbacks.get(key); let currentCallbacks = callbacks.get(key);
if (!currentCallbacks) { if (!currentCallbacks) {
currentCallbacks = new Set(); currentCallbacks = new Set();
@ -164,14 +203,13 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
const resultTypes = searchTypesToResultTypes(searchType); const resultTypes = searchTypesToResultTypes(searchType);
const [selected, setSelected] = useState<DestinationItemProps[]>([]); const [selected, setSelected] = useState<DestinationItem[]>([]);
const refCounter = useRef(0); const refCounter = useRef(0);
const rowContext = React.createContext({ const rowContext = React.createContext({
id: "NO_LIST", id: "NO_LIST",
setFocus(id: string) { setFocus(id: string) {}
}
}); });
const Row = (props: SpecificRowProps) => { const Row = (props: SpecificRowProps) => {
@ -196,7 +234,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
return ( return (
<SearchBarModule.Clickable <SearchBarModule.Clickable
className={cl("destination-row")} className={cl("destination-row")}
onClick={e => { handlePress(); e.preventDefault(); e.stopPropagation(); }} onClick={e => { e.stopPropagation(); e.preventDefault(); handlePress(); }}
aria-selected={isSelected} aria-selected={isSelected}
{...interactionProps} {...interactionProps}
{...rest} {...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); return getChannelLabel(channel, UserStore, RelationshipStore, false);
} }
@ -257,7 +295,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
const channelLabel = generateChannelLabel(channel); const channelLabel = generateChannelLabel(channel);
const parentChannelLabel = () => { const parentChannelLabel = (): string => {
const parentChannel = ChannelStore.getChannel(channel.parent_id); const parentChannel = ChannelStore.getChannel(channel.parent_id);
return parentChannel ? getChannelLabel(parentChannel, UserStore, RelationshipStore, false) : null; 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)); .map(user => UsernameUtils.getName(user));
if (!userNames || userNames.length === 0 || channel.name === "") if (!userNames || userNames.length === 0 || channel.name === "")
return null; return "";
if (userNames.length <= 3) if (userNames.length <= 3)
return userNames.join(", "); return userNames.join(", ");
const amount = userNames.length - 3; const amount = userNames.length - 3;
@ -325,7 +363,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
icon={<GroupDMAvatars aria-hidden={true} size={SearchBarModule.AvatarSizes.SIZE_32} icon={<GroupDMAvatars aria-hidden={true} size={SearchBarModule.AvatarSizes.SIZE_32}
channel={channel}/>} channel={channel}/>}
label={label} 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<typeof generateNavigatorData>) => React.ReactNode }): React.ReactNode {
const { children } = e; const { children } = e;
return children(generateNavigatorData()); return children(generateNavigatorData());
} }
@ -364,7 +402,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
const handleFocus = useCallback(() => setFocus(rowId), [rowId, setFocus]); const handleFocus = useCallback(() => setFocus(rowId), [rowId, setFocus]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
return registerCallback(id, (tabIndex, id) => { return registerCallback(id, (tabIndex: string, id: string) => {
setTabIndex(id && tabIndex === rowId ? 0 : -1); setTabIndex(id && tabIndex === rowId ? 0 : -1);
}); });
}, [rowId, id]); }, [rowId, id]);
@ -380,22 +418,21 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
const [searchText, setSearchText] = useState<string>(input || ""); const [searchText, setSearchText] = useState<string>(input || "");
const ref = {}; const ref = {};
function getSearchHandler(e) { function getSearchHandler(searchOptions: Record<string, any>): { search: (e: { query: string, resultTypes: string[] }) => void, results: Result[], query: string } {
const { searchOptions } = e; const [results, setResults] = useState<{ results: Result[], query: string }>({
const [results, setResults] = useState({
results: [], results: [],
query: "" query: ""
}); });
function getRef(e) { // FIXME probably should use a proper type for this function getRef<T>(e: () => T): T {
const ref_ = useRef(ref); const ref_ = useRef<T>(ref as T);
if (ref_.current === ref) if (ref_.current === ref)
ref_.current = e(); ref_.current = e();
return ref_.current; return ref_.current;
} }
const searchHandler: typeof SearchHandler = getRef(() => { const searchHandler: InstanceType<typeof SearchHandler> = getRef(() => {
const searchHandler = new SearchHandler((r, q) => { const searchHandler = new SearchHandler((r: Result[], q: string) => {
setResults({ setResults({
results: r, results: r,
query: q 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({ const { search, query, results } = getSearchHandler({
blacklist: null, blacklist: null,
frecencyBoosters: !0, frecencyBoosters: !0,
@ -454,10 +491,9 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
loadFunction(); loadFunction();
const frequentChannels = wrapperFn([FrequencyModule], () => FrequencyModule.getFrequentlyWithoutFetchingLatest()); const frequentChannels = wrapperFn([FrequencyModule], () => FrequencyModule.getFrequentlyWithoutFetchingLatest());
const isConnected = wrapperFn([ConnectionModule], () => ConnectionModule.isConnected());
const hasQuery = query !== ""; const hasQuery = query !== "";
function getItem(e) { function getItem(e: DestinationItem): Result {
if (e.type !== "user") if (e.type !== "user")
return convertItem(e.id); 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[]) => { const filterItems = (items: any[]) => {
return items.filter( return items.filter(
item => item != null && (item.type === TextTypes.HEADER || resultTypes.includes(item.type)) item => item != null && resultTypes.includes(item.type)
); );
}; };
function filterResults(e) { function filterResults(props: {
const removeDuplicates = (arr: any[]) => { results: Result[];
hasQuery: boolean;
frequentChannels: Channel[];
pinnedDestinations: DestinationItem[];
}): Result[] {
const removeDuplicates = (arr: Result[]): Result[] => {
const clean: any[] = []; const clean: any[] = [];
const seenIds = new Set(); const seenIds = new Set();
arr.forEach(item => { arr.forEach(item => {
if (item == null || item.record == null) return; if (item == null || item.record == null) return;
const id = item.type === "user" ? item.id : item.record.id; if (!seenIds.has(item.record.id)) {
if (!seenIds.has(id)) { seenIds.add(item.record.id);
seenIds.add(id);
clean.push(item); clean.push(item);
} }
}); });
return clean; return clean;
}; };
const { results, hasQuery, frequentChannels, pinnedDestinations } = e; const { results, hasQuery, frequentChannels, pinnedDestinations } = props;
if (hasQuery) return processItems(filterItems(results)); if (hasQuery) return filterItems(results);
const channelHistory = FrequentsModule.getChannelHistory(); const channelHistory: string[] = FrequentsModule.getChannelHistory();
const recentDestinations = filterItems([ const recentDestinations = filterItems([
...(channelHistory.length > 0 ? channelHistory.map(e => convertItem(e)) : []), ...(channelHistory.length > 0 ? channelHistory.map(e => convertItem(e)) : []),
...(frequentChannels.length > 0 ? frequentChannels.map(e => convertItem(e.id)) : []) ...(frequentChannels.length > 0 ? frequentChannels.map(e => convertItem(e.id)) : [])
]); ]);
const destinations = removeDuplicates( return removeDuplicates(
[...(pinnedDestinations.length > 0 ? pinnedDestinations.map(e => getItem(e)) : []), [...(pinnedDestinations.length > 0 ? pinnedDestinations.map(e => getItem(e)) : []),
...recentDestinations ...recentDestinations
]); ]);
return processItems(destinations);
} }
return { return {
@ -542,8 +555,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
hasQuery: hasQuery, hasQuery: hasQuery,
frequentChannels: frequentChannels, frequentChannels: frequentChannels,
pinnedDestinations: pinned, pinnedDestinations: pinned,
isConnected: isConnected }), [results, hasQuery, frequentChannels, pinned]),
}), [results, hasQuery, frequentChannels, pinned, isConnected]),
updateSearchText: updateSearch updateSearchText: updateSearch
}; };
} }
@ -558,12 +570,11 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
const rowHeight = useCallback(() => 48, []); 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: number[] = useMemo(() => [rowData.length], [rowData.length]);
const sectionCount = useMemo(() => [t.length], [t.length]);
const callback = useCallback(e => { const callback = useCallback((e: { section: number, row: number }) => {
const { section, row } = e; const { section, row } = e;
if (section > 0) if (section > 0)
return; return;
@ -571,8 +582,8 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
if (type === TextTypes.HEADER) if (type === TextTypes.HEADER)
return; return;
const destination = { const destination: DestinationItem = {
type: type === TextTypes.USER ? "user" : "channel", type: type === TextTypes.USER ? "user" : type === TextTypes.GUILD ? "guild" : "channel",
id: record.id id: record.id
}; };
@ -590,11 +601,11 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
"aria-setsize": results.length "aria-setsize": results.length
}; };
if (type === TextTypes.USER) if (type === "USER")
return generateUserItem(record, rowProps); return generateUserItem(record, rowProps);
if (type === TextTypes.GROUP_DM) if (type === "GROUP_DM")
return generateGdmItem(record, rowProps); return generateGdmItem(record, rowProps);
if (type === TextTypes.TEXT_CHANNEL || type === TextTypes.VOICE_CHANNEL) { if (type === "TEXT_CHANNEL" || type === "VOICE_CHANNEL") {
return generateChannelItem(record, rowProps); return generateChannelItem(record, rowProps);
} else throw new Error("Unknown type " + type); } else throw new Error("Unknown type " + type);
}, [results, selectedDestinationKeys, handleToggleDestination]); }, [results, selectedDestinationKeys, handleToggleDestination]);
@ -614,7 +625,8 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
} }
} }
{...data} {...data}
{...extraProps} paddingBottom={paddingBottom}
paddingTop={paddingTop}
sections={sectionCount} sections={sectionCount}
sectionHeight={0} sectionHeight={0}
renderRow={callback} renderRow={callback}
@ -625,8 +637,8 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
} }
const setSelectedCallback = useCallback(e => { const setSelectedCallback = useCallback((e: DestinationItem) => {
setSelected(currentSelected => { setSelected((currentSelected: DestinationItem[]) => {
const index = currentSelected.findIndex(item => { const index = currentSelected.findIndex(item => {
const { type, id } = item; const { type, id } = item;
return type === e.type && id === e.id; return type === e.type && id === e.id;