From a6184f57dbc43fba3efb4b41adbd496edda8ed27 Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:12:46 -0800 Subject: [PATCH] This better not break again for at least a year. (#702) --- src/equicordplugins/questify/index.tsx | 34 +++- src/equicordplugins/questify/settings.tsx | 155 ++++++++++-------- src/equicordplugins/questify/styles.css | 10 +- .../questify/utils/components.tsx | 1 + 4 files changed, 123 insertions(+), 77 deletions(-) diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index a00811453..eb36c1520 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -1351,12 +1351,16 @@ export default definePlugin({ ] }, { - // Adds a feedback prop to the SearchableSelect component which will display on invalid searches. + // MARK: TODO + // - Cleanup once Discord rolls out the new mana select completely. + // - Also see anywhere DynamicDropdown is used for refactoring. + // + // Various patches to the SearchableSelect component. find: '"onSearchChange",', group: true, replacement: [ { - // Extracts the custom dropdown prop before the variable is overwritten. + // Extracts a custom feedback prop before the variable is overwritten. match: /(?<=forwardRef\(function\((\i),\i\){)/, replace: "const vcDynamicDropdownFeedback=$1.feedback;" }, @@ -1374,6 +1378,32 @@ export default definePlugin({ // Prevent SearchableSelect from force-scrolling into view, causing the dropdown to close. match: /(?<=\.scrollIntoView\()/, replace: "{block:\"nearest\",inline:\"nearest\"}" + }, + { + // Passes a popoutClassName and optionClassName to the popout handler. + match: /(?<=renderOptionPrefix:\i,renderOptionSuffix:\i)/, + replace: ",popoutClassName:arguments[0]?.popoutClassName,optionClassName:arguments[0]?.optionClassName" + }, + { + // Makes use of the custom popoutClassName prop if provided. + match: /(?<=onKeyDown"]\);return.{0,30}?className:\i\(\)\()/, + replace: "arguments[0]?.popoutClassName," + }, + { + // Passes the custom optionClassName prop to the row renderer. + match: /(?<="aria-posinset":\i,"aria-setsize":\i.length,)/, + replace: "optionClassName:arguments[0]?.optionClassName," + }, + { + // Makes use of the custom optionClassName prop if provided. + match: /(?<=focusProps:{enabled:!1},className:\i\(\)\()/, + replace: "arguments[0]?.optionClassName," + }, + { + // Pass the unused props to the new mana select being + // used by dev://experiment/2025-09-mana-desktop-select + match: /(?<=closeOnSelect:\i)(?=})/, + replace: ",...arguments[0]" } ] }, diff --git a/src/equicordplugins/questify/settings.tsx b/src/equicordplugins/questify/settings.tsx index aab19ecdb..eebed79bd 100644 --- a/src/equicordplugins/questify/settings.tsx +++ b/src/equicordplugins/questify/settings.tsx @@ -448,7 +448,7 @@ function QuestButtonSettings(): JSX.Element { { label: "Profile Collectibles", value: "collectibles", selected: questRewardIncludeCollectibles }, ]; - const [currentRewardsOptions, setCurrentRewardsOptions] = useState(questButtonRewardDisplayOptions.filter(option => option.selected)); + const [currentRewardsOptions, setCurrentRewardsOptions] = useState(questButtonRewardDisplayOptions.filter(option => option.selected).map(option => option.value)); const [currentQuestButtonDisplay, setCurrentQuestButtonDisplay] = useState((questButtonDisplayOptions.find(option => option.value === questButtonDisplay) as RadioOption)); const [currentQuestButtonUnclaimed, setCurrentQuestButtonUnclaimed] = useState((questButtonUnclaimedOptions.find(option => option.value === questButtonUnclaimed) as RadioOption)); const [currentQuestButtonLeftClickAction, setCurrentQuestButtonLeftClickAction] = useState<"open-quests" | "plugin-settings" | "context-menu" | "nothing">(questButtonLeftClickAction as "open-quests" | "plugin-settings" | "context-menu" | "nothing"); @@ -457,37 +457,37 @@ function QuestButtonSettings(): JSX.Element { const [currentBadgeColor, setCurrentBadgeColor] = useState((questButtonBadgeColor as number | null)); const [dummySelected, setDummySelected] = useState(false); - function updateSettingsTruthy(enabled: DynamicDropdownSettingOption[]) { - const enabledValues = enabled.map(option => option.value); - + function updateSettingsTruthy(enabled: string[]) { questButtonRewardDisplayOptions.forEach(option => { - option.selected = enabledValues.includes(option.value); + option.selected = enabled.includes(option.value); }); - settings.store.questRewardIncludeRewardCode = enabledValues.includes("reward-code"); - settings.store.questRewardIncludeNitroCode = enabledValues.includes("nitro-code"); - settings.store.questRewardIncludeCollectibles = enabledValues.includes("collectibles"); - settings.store.questRewardIncludeInGame = enabledValues.includes("in-game"); - settings.store.questRewardIncludeOrbs = enabledValues.includes("orbs"); + settings.store.questRewardIncludeRewardCode = enabled.includes("reward-code"); + settings.store.questRewardIncludeNitroCode = enabled.includes("nitro-code"); + settings.store.questRewardIncludeCollectibles = enabled.includes("collectibles"); + settings.store.questRewardIncludeInGame = enabled.includes("in-game"); + settings.store.questRewardIncludeOrbs = enabled.includes("orbs"); setCurrentRewardsOptions(enabled); validateAndOverwriteIgnoredQuests(); } function handleQuestRewardDisplayChange(values: Array) { + const parsedValues = values.map(v => typeof v === "string" ? questButtonRewardDisplayOptions.find(option => option.value === v) as DynamicDropdownSettingOption : v); + if (values.length === 0) { updateSettingsTruthy([]); return; } - const stringlessValues = values.filter(v => typeof v !== "string") as DynamicDropdownSettingOption[]; - const selectedOption = values.find(v => typeof v === "string") as string; + const selectedOption = values.length > currentRewardsOptions.length ? values[values.length - 1] as string : currentRewardsOptions.find(v => !values.includes(v)) as string; + const stringlessValues = parsedValues.filter(v => v.value !== selectedOption); const option = questButtonRewardDisplayOptions.find(option => option.value === selectedOption) as DynamicDropdownSettingOption; if (option.selected) { - updateSettingsTruthy(stringlessValues.filter(v => v.value !== selectedOption)); + updateSettingsTruthy(stringlessValues.filter(v => v.value !== selectedOption).map(v => v.value)); } else { - updateSettingsTruthy([...stringlessValues, option]); + updateSettingsTruthy([...stringlessValues.map(v => v.value), option.value]); } } @@ -641,7 +641,7 @@ function QuestButtonSettings(): JSX.Element { Included Reward Types - + Only count Quests with these reward types as unclaimed when determining button visibility, badge count, and when playing the alert sound. @@ -649,10 +649,12 @@ function QuestButtonSettings(): JSX.Element { placeholder="Select which reward types to include in the unclaimed count..." feedback="There's no supported Quest feature by that name." className={q("select")} + optionClassName={q("affixless-select-popout-option")} maxVisibleItems={questButtonRewardDisplayOptions.length} + maxOptionsVisible={questButtonRewardDisplayOptions.length} // Mana Select Prop clearable={true} multi={true} - value={currentRewardsOptions as any} + value={currentRewardsOptions} options={questButtonRewardDisplayOptions} onChange={handleQuestRewardDisplayChange} closeOnSelect={false} @@ -724,70 +726,73 @@ function DisableQuestsSetting(): JSX.Element { const disableOptions = options.filter(option => option.type === "disable"); const modificationOptions = options.filter(option => option.type === "modification"); const everythingOnly = options.find(option => option.value === "everything") as DynamicDropdownSettingOption; - const [currentValue, setCurrentValue] = useState(options.filter(option => option.selected)); - - function updateSettingsTruthy(enabled: DynamicDropdownSettingOption[]) { - const enabledValues = enabled.map(option => option.value); + const [currentValue, setCurrentValue] = useState(options.filter(option => option.selected).map(option => option.value)); + function updateSettingsTruthy(enabled: string[]) { options.forEach(option => { - option.selected = enabledValues.includes(option.value); + option.selected = enabled.includes(option.value); }); const redoAutoFetch = ( - settings.store.disableQuestsEverything !== enabledValues.includes("everything") || - settings.store.disableQuestsFetchingQuests !== enabledValues.includes("fetching") + settings.store.disableQuestsEverything !== enabled.includes("everything") || + settings.store.disableQuestsFetchingQuests !== enabled.includes("fetching") ); - settings.store.disableQuestsEverything = enabledValues.includes("everything"); - settings.store.disableQuestsDiscoveryTab = enabledValues.includes("discovery"); - settings.store.disableQuestsFetchingQuests = enabledValues.includes("fetching"); - settings.store.disableQuestsDirectMessagesTab = enabledValues.includes("dms"); - settings.store.disableQuestsPageSponsoredBanner = enabledValues.includes("sponsored-banner"); - settings.store.disableQuestsPopupAboveAccountPanel = enabledValues.includes("popup"); - settings.store.disableQuestsBadgeOnUserProfiles = enabledValues.includes("badge"); - settings.store.disableQuestsGiftInventoryRelocationNotice = enabledValues.includes("inventory"); - settings.store.disableFriendsListActiveNowPromotion = enabledValues.includes("friends-list"); - settings.store.disableMembersListActivelyPlayingIcon = enabledValues.includes("members-list"); - settings.store.makeMobileQuestsDesktopCompatible = enabledValues.includes("mobile-desktop-compatible"); - settings.store.completeVideoQuestsInBackground = enabledValues.includes("video-quests-background"); - settings.store.completeGameQuestsInBackground = enabledValues.includes("game-quests-background"); - settings.store.completeAchievementQuestsInBackground = enabledValues.includes("achievement-quests-background"); - settings.store.notifyOnQuestComplete = enabledValues.includes("notify-on-complete"); + settings.store.disableQuestsEverything = enabled.includes("everything"); + settings.store.disableQuestsDiscoveryTab = enabled.includes("discovery"); + settings.store.disableQuestsFetchingQuests = enabled.includes("fetching"); + settings.store.disableQuestsDirectMessagesTab = enabled.includes("dms"); + settings.store.disableQuestsPageSponsoredBanner = enabled.includes("sponsored-banner"); + settings.store.disableQuestsPopupAboveAccountPanel = enabled.includes("popup"); + settings.store.disableQuestsBadgeOnUserProfiles = enabled.includes("badge"); + settings.store.disableQuestsGiftInventoryRelocationNotice = enabled.includes("inventory"); + settings.store.disableFriendsListActiveNowPromotion = enabled.includes("friends-list"); + settings.store.disableMembersListActivelyPlayingIcon = enabled.includes("members-list"); + settings.store.makeMobileQuestsDesktopCompatible = enabled.includes("mobile-desktop-compatible"); + settings.store.completeVideoQuestsInBackground = enabled.includes("video-quests-background"); + settings.store.completeGameQuestsInBackground = enabled.includes("game-quests-background"); + settings.store.completeAchievementQuestsInBackground = enabled.includes("achievement-quests-background"); + settings.store.notifyOnQuestComplete = enabled.includes("notify-on-complete"); redoAutoFetch ? checkAutoFetchInterval(settings.store.fetchingQuestsInterval) : null; - setCurrentValue(enabled); + setCurrentValue(enabled.map(value => typeof value === "string" ? value : (value as any).value)); } function handleChange(values: Array) { + const parsedValues = values.map(v => typeof v === "string" ? options.find(option => option.value === v) as DynamicDropdownSettingOption : v); + if (values.length === 0) { updateSettingsTruthy([]); return; } - const stringlessValues = values.filter(v => typeof v !== "string") as DynamicDropdownSettingOption[]; - const selectedOption = values.find(v => typeof v === "string") as string; + const selectedOption = values.length > currentValue.length ? values[values.length - 1] as string : currentValue.find(v => !values.includes(v)) as string; + const stringlessValues = parsedValues.filter(v => v.value !== selectedOption); if (selectedOption === "everything") { - if (everythingOnly.selected) { // If was already selected when clicked. - updateSettingsTruthy([...stringlessValues.filter(option => option.type !== "disable")]); + if (everythingOnly.selected) { + // If was already selected when clicked. + updateSettingsTruthy(stringlessValues.filter(option => option.value !== selectedOption).map(option => option.value)); } else { - updateSettingsTruthy([...stringlessValues.filter(option => option.type !== "disable"), everythingOnly]); + updateSettingsTruthy([...stringlessValues.filter(option => option.type !== "disable").map(option => option.value), selectedOption]); } } else if (disableOptions.some(option => option.value === selectedOption)) { const option = disableOptions.find(option => option.value === selectedOption) as DynamicDropdownSettingOption; - if (option.selected) { // If was already selected when clicked. - updateSettingsTruthy(stringlessValues.filter(option => option.value !== selectedOption && option.value !== everythingOnly.value)); + if (option.selected) { + // If was already selected when clicked. + updateSettingsTruthy(stringlessValues.filter(option => option.value !== selectedOption).map(option => option.value)); } else { - updateSettingsTruthy([...stringlessValues.filter(option => option.value !== everythingOnly.value), option]); + updateSettingsTruthy([...stringlessValues.map(option => option.value), selectedOption]); } } else if (modificationOptions.some(option => option.value === selectedOption)) { const option = modificationOptions.find(option => option.value === selectedOption) as DynamicDropdownSettingOption; - if (option.selected) { // If was already selected when clicked. - updateSettingsTruthy(stringlessValues.filter(option => option.value !== selectedOption)); + if (option.selected) { + // If was already selected when clicked. + updateSettingsTruthy(stringlessValues.map(option => option.value)); } else { - updateSettingsTruthy([...stringlessValues, option]); + updateSettingsTruthy([...stringlessValues.map(option => option.value), selectedOption]); } } } @@ -800,7 +805,7 @@ function DisableQuestsSetting(): JSX.Element { Quest Features - + Modify specific Quest features.

The Disable Quest Popup Above Account Panel option @@ -827,10 +832,12 @@ function DisableQuestsSetting(): JSX.Element { placeholder="Select which Quest features to modify." feedback="There's no supported Quest feature by that name." className={q("select")} + optionClassName={q("affixless-select-popout-option")} maxVisibleItems={options.length} + maxOptionsVisible={options.length} // Mana Select Prop clearable={true} multi={true} - value={currentValue as any} + value={currentValue} options={options} onChange={handleChange} closeOnSelect={false} @@ -1350,24 +1357,27 @@ function FetchingQuestsSetting(): JSX.Element { }; } - function getAllIntervalOptions(currentValue: SelectOption) { - const otherOptions = resolvedIntervals.filter(option => option.value !== currentValue.value); + function getAllIntervalOptions(currentValue: number) { + const currentOption = createIntervalSelectOptionFromValue(currentValue); + const otherOptions = resolvedIntervals.filter(option => option.value !== currentValue); return [ - currentValue, + currentOption, ...otherOptions ].sort((a, b) => Number(a.value) - Number(b.value)); } - function getAllAlertOptions(currentValue: SelectOption | null) { - const otherOptions = currentValue ? resolvedSounds.filter(option => option.value !== currentValue.value) : resolvedSounds; + function getAllAlertOptions(currentValue: string | null) { + const otherOptions = currentValue ? resolvedSounds.filter(option => option.value !== currentValue) : resolvedSounds; if (!currentValue) { return otherOptions.sort((a, b) => a.label.localeCompare(b.label)); } + const currentOption = createAlertSelectOptionFromValue(currentValue); + return [ - currentValue, + currentOption!, ...otherOptions ].sort((a, b) => a.label.localeCompare(b.label)); } @@ -1466,10 +1476,10 @@ function FetchingQuestsSetting(): JSX.Element { } const resolvedIntervalValue = fetchingQuestsInterval; - const [currentIntervalSelection, setCurrentSelection] = useState(createIntervalSelectOptionFromValue(resolvedIntervalValue)); + const [currentIntervalSelection, setCurrentSelection] = useState(resolvedIntervalValue); const [currentIntervalOptions, setCurrentIntervalOptions] = useState(getAllIntervalOptions(currentIntervalSelection)); const resolvedAlertValue = fetchingQuestsAlert; - const [currentAlertSelection, setCurrentAlertSelection] = useState(createAlertSelectOptionFromValue(resolvedAlertValue)); + const [currentAlertSelection, setCurrentAlertSelection] = useState(resolvedAlertValue); const [currentAlertOptions, setCurrentAlertOptions] = useState(getAllAlertOptions(currentAlertSelection)); // Needed to update the playing state of the preview button. const [isPlaying, setIsPlaying] = useState(false); @@ -1523,6 +1533,7 @@ function FetchingQuestsSetting(): JSX.Element { feedback="Intervals must be between 30 minutes and 12 hours." className={q("select", "titled-select")} maxVisibleItems={resolvedIntervals.length + 1} + maxOptionsVisible={resolvedIntervals.length + 1} clearable={false} multi={false} value={currentIntervalSelection as any} @@ -1531,12 +1542,12 @@ function FetchingQuestsSetting(): JSX.Element { onSearchChange={handleScaleSearchChange} onClose={() => { setCurrentIntervalOptions(getAllIntervalOptions(currentIntervalSelection)); }} onChange={value => { - const option = currentIntervalOptions.find(o => o.value === value) as SelectOption; - settings.store.fetchingQuestsInterval = option.value as number; + const val = value as number; + settings.store.fetchingQuestsInterval = val; - setCurrentSelection(option); - setCurrentIntervalOptions(getAllIntervalOptions(option)); - checkAutoFetchInterval(option.value as number); + setCurrentSelection(val); + setCurrentIntervalOptions(getAllIntervalOptions(val)); + checkAutoFetchInterval(val); }} /> @@ -1563,27 +1574,27 @@ function FetchingQuestsSetting(): JSX.Element { onSearchChange={handleAlertSearchChange} onClose={() => { setCurrentAlertOptions(getAllAlertOptions(currentAlertSelection)); }} onChange={value => { - const option = currentAlertOptions.find(o => o.value === value) as SelectOption; - settings.store.fetchingQuestsAlert = value ? option.value as string : null as any; + const val = value as string | null; + settings.store.fetchingQuestsAlert = val as any; - setCurrentAlertSelection(value ? option : null); - setCurrentAlertOptions(getAllAlertOptions(option)); + setCurrentAlertSelection(val); + setCurrentAlertOptions(getAllAlertOptions(val)); }} />
{ - if (currentAlertSelection?.value) { + if (currentAlertSelection) { if (activePlayer.current) { clearActivePlayer(); } else { - activePlayer.current = playAudio(currentAlertSelection.value as string, { onEnded: clearActivePlayer, volume: settings.store.fetchingQuestsAlertVolume }); + activePlayer.current = playAudio(currentAlertSelection, { onEnded: clearActivePlayer, volume: settings.store.fetchingQuestsAlertVolume }); setIsPlaying(true); } } }} - style={{ cursor: currentAlertSelection && currentAlertSelection.value ? "pointer" : "default" }} + style={{ cursor: currentAlertSelection ? "pointer" : "default" }} > {SoundIcon(24, 24)}
diff --git a/src/equicordplugins/questify/styles.css b/src/equicordplugins/questify/styles.css index c40581519..35235386a 100644 --- a/src/equicordplugins/questify/styles.css +++ b/src/equicordplugins/questify/styles.css @@ -1,4 +1,8 @@ /* Plugin Settings */ +.questify-margin-bottom-8 { + margin-bottom: 8px; +} + .questify-setting-divider { margin: 0; } @@ -50,11 +54,11 @@ border-radius: 3px; min-height: 43px; border: 1px solid var(--border-normal); - margin-top: 8px; } -.questify-titled-select > div { - margin-top: 0; +.questify-affixless-select-popout-option { + grid-template-areas: "content selectedIcon" !important; + grid-template-columns: 1fr auto !important; } .questify-volume-slider-container { diff --git a/src/equicordplugins/questify/utils/components.tsx b/src/equicordplugins/questify/utils/components.tsx index a39a727ea..afc1a8ceb 100644 --- a/src/equicordplugins/questify/utils/components.tsx +++ b/src/equicordplugins/questify/utils/components.tsx @@ -343,6 +343,7 @@ export function GuildlessServerListItem({ export interface DynamicDropdownProps extends React.ComponentProps { feedback?: string; + maxOptionsVisible?: number; } export const DynamicDropdown: React.FC = props => {