This better not break again for at least a year. (#702)

This commit is contained in:
Etorix 2026-01-28 15:12:46 -08:00 • committed by GitHub
parent 7e92fd94a3
commit a6184f57db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 123 additions and 77 deletions

View file

@ -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]"
}
]
},

View file

@ -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<DynamicDropdownSettingOption | string>) {
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 {
<Heading className={q("form-subtitle", "form-subtitle-spacier")}>
Included Reward Types
</Heading>
<Paragraph className={q("form-description")}>
<Paragraph className={q("form-description", "margin-bottom-8")}>
Only count Quests with these reward types as unclaimed when determining button
visibility, badge count, and when playing the alert sound.
</Paragraph>
@ -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<DynamicDropdownSettingOption | string>) {
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 {
<Heading className={q("form-title")}>
Quest Features
</Heading>
<Paragraph className={q("form-description")}>
<Paragraph className={q("form-description", "margin-bottom-8")}>
Modify specific Quest features.
<br /><br />
The <span className={q("inline-code-block")}>Disable Quest Popup Above Account Panel</span> 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<SelectOption | null>(createAlertSelectOptionFromValue(resolvedAlertValue));
const [currentAlertSelection, setCurrentAlertSelection] = useState<string | null>(resolvedAlertValue);
const [currentAlertOptions, setCurrentAlertOptions] = useState(getAllAlertOptions(currentAlertSelection));
// Needed to update the playing state of the preview button.
const [isPlaying, setIsPlaying] = useState<boolean>(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);
}}
/>
</div>
@ -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));
}}
/>
</div>
<div
className={q("inline-group-item", "alert-icon", { "playing-audio": !!isPlaying })}
onClick={() => {
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)}
</div>

View file

@ -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 {

View file

@ -343,6 +343,7 @@ export function GuildlessServerListItem({
export interface DynamicDropdownProps extends React.ComponentProps<typeof SearchableSelect> {
feedback?: string;
maxOptionsVisible?: number;
}
export const DynamicDropdown: React.FC<DynamicDropdownProps> = props => {