/* * Vencord, a Discord client mod * Copyright (c) 2026 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import { plugins } from "@api/PluginManager"; import { ErrorBoundary } from "@components/index"; import { findComponentByCodeLazy } from "@webpack"; import { ContextMenuApi, Menu, NavigationRouter, useState } from "@webpack/common"; import type { CSSProperties, JSX, MouseEvent } from "react"; import { getQuestifySettings, useQuestifySettings } from "../settings/access"; import type { QuestButtonAction, QuestButtonDisplayMode, QuestButtonIndicatorMode } from "../settings/def"; import { getIgnoredQuestIDs, ignoreAllQuests, resetIgnoredQuests } from "../settings/ignoredQuests"; import { rerenderQuests } from "../settings/rerender"; import { initialQuestDataFetched } from "../state"; import { getActiveAutoCompletes, getQueueableAutoCompleteQuests, isQuestEnrollmentRateLimited, isQueueAllAutoCompleteQuestsInProgress, queueAllAutoCompleteQuests, stopAllAutoCompletes } from "../utils/completion"; import { fetchAndAlertQuests } from "../utils/fetching"; import { decimalToRGB, formatLowerBadge, isDarkish, leftClick, middleClick, q, QUEST_PAGE, rightClick } from "../utils/ui"; import { openQuestifySettingsModal } from "./settingsModal"; const GuildlessServerListItemComponent = findComponentByCodeLazy("tooltip:", "lowerBadgeSize:"); const ServerListItemPillComponent = findComponentByCodeLazy("=!1,hovered:", "=!1,unread:", "=!1,disabled:"); const ServerListItemLowerBadgeComponent = findComponentByCodeLazy("BADGE_NOTIFICATION_BACKGROUND", "let{count:"); interface QuestButtonLowerBadgeProps { count: number; color?: string; style?: CSSProperties; maxDigits?: number; } interface QuestButtonViewProps { id: string; className: string; badgeProps: QuestButtonLowerBadgeProps; hasUnread: boolean; selected: boolean; visible: boolean; onPress: (event: MouseEvent) => void; } export function QuestIcon(height: number, width: number, className?: string): JSX.Element { return ( ); } function splitClassNames(className: string): string[] { const classNames = className.split(/\s+/).filter(Boolean); return classNames.length ? classNames : [q("quest-button")]; } function suffixClassNames(classNames: string[], suffix: string): string { return classNames.map(className => `${className}-${suffix}`).join(" "); } function QuestButtonLowerBadge(props: QuestButtonLowerBadgeProps & { className: string; }): JSX.Element { return ; } function QuestButtonView({ id, className, badgeProps, hasUnread, selected, visible, onPress, }: QuestButtonViewProps): JSX.Element { const [hovered, setHovered] = useState(false); const baseClasses = splitClassNames(className); const mainContainerClass = suffixClassNames(baseClasses, "container"); const iconContainerClass = suffixClassNames(baseClasses, "icon-container"); const buttonContainerClass = suffixClassNames(baseClasses, "server-list-button-container"); const pillContainerClass = suffixClassNames(baseClasses, "pill-container"); const buttonClass = suffixClassNames(baseClasses, "button"); const pillClass = suffixClassNames(baseClasses, "pill"); const lowerBadgeClass = suffixClassNames(baseClasses, "lower-badge"); const lowerBadgeSize = { width: formatLowerBadge(badgeProps.count, badgeProps.maxDigits)[1] }; const lowerBadge = badgeProps.count === 0 ? null : ( ); const icon = (
{QuestIcon(26, 26)}
); return ( {visible && (
icon} tooltip="Quests" showPill={false} selected={selected} className={buttonClass} onClick={onPress} onMouseDown={onPress} onContextMenu={onPress} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} {...(lowerBadge ? { lowerBadge, lowerBadgeSize } : {})} />
)}
); } export function showQuestButton(displayMode: QuestButtonDisplayMode, badgeCount: number, isOnQuestsPage: boolean): boolean { return displayMode !== "never" && (displayMode === "always" || badgeCount > 0 || isOnQuestsPage); } export function disguiseHomeButton(pathname: string): boolean { const buttonSettings = useQuestifySettings(["questButtonDisplay", "questButtonBadgeCount", "isOnQuestsPage"]); if (!showQuestButton(buttonSettings.questButtonDisplay, buttonSettings.questButtonBadgeCount, buttonSettings.isOnQuestsPage)) { return false; } return pathname === QUEST_PAGE; } function resolveQuestButtonAction( event: MouseEvent, leftAction: QuestButtonAction, middleAction: QuestButtonAction, rightAction: QuestButtonAction, ): QuestButtonAction | null { if (event.button === middleClick) { return middleAction; } if (event.button === rightClick) { return rightAction; } if (event.button === leftClick) { return leftAction; } return null; } function getQuestButtonLowerBadgeProps( badgeCount: number, indicatorMode: QuestButtonIndicatorMode, badgeColor: number | null, ): QuestButtonLowerBadgeProps { const resolvedColor = badgeColor === null ? null : decimalToRGB(badgeColor); return { count: indicatorMode === "badge" || indicatorMode === "both" ? badgeCount : 0, maxDigits: 2, ...(resolvedColor ? { color: `rgb(${resolvedColor.r}, ${resolvedColor.g}, ${resolvedColor.b})` } : {}), ...(resolvedColor ? { style: { color: isDarkish(resolvedColor) ? "white" : "black" } } : {}), }; } export function QuestButtonContextMenu({ dummy = false }: { dummy?: boolean; }): JSX.Element { const navId = q(dummy ? "dummy-quest-button-context-menu" : "quest-button-context-menu"); const settings = getQuestifySettings(); const markAllIgnoredDisabled = dummy || settings.questButtonBadgeCount <= 0; const resetIgnoredDisabled = dummy || getIgnoredQuestIDs().length <= 0; const noneAutoCompletable = getQueueableAutoCompleteQuests().length <= 0; const queueAllQuestsVisible = !settings.autoCompleteQuestsSimultaneously && !noneAutoCompletable; const enrollmentRateLimited = isQuestEnrollmentRateLimited(); const queueAllQuestsInProgress = isQueueAllAutoCompleteQuestsInProgress(); const queueAllQuestsDisabled = dummy || queueAllQuestsInProgress || enrollmentRateLimited; const labelSuffix = (dummy ? "" : queueAllQuestsInProgress ? " (In-Progress)" : enrollmentRateLimited ? " (Rate-Limited)" : ""); const clearQueueVisible = queueAllQuestsInProgress || getActiveAutoCompletes().length > 0; const fetchQuestsDisabled = dummy; return ( {queueAllQuestsVisible && ( { const queueAllPromise = queueAllAutoCompleteQuests(); rerenderQuests(); void queueAllPromise.finally(() => { rerenderQuests(); }); }} disabled={queueAllQuestsDisabled} /> )} {clearQueueVisible && ( { stopAllAutoCompletes({ manual: true, preserveResume: false, terminalHeartbeat: true, }); rerenderQuests(); }} disabled={dummy} /> )} { void fetchAndAlertQuests("MANUAL_FETCH"); }} disabled={fetchQuestsDisabled} /> ); } export function QuestButton(): JSX.Element { const { isOnQuestsPage, questButtonBadgeColor, questButtonDisplay, questButtonBadgeCount, questButtonLeftClickAction, questButtonMiddleClickAction, questButtonRightClickAction, questButtonIndicator, } = useQuestifySettings([ "isOnQuestsPage", "questButtonBadgeColor", "questButtonDisplay", "questButtonBadgeCount", "questButtonLeftClickAction", "questButtonMiddleClickAction", "questButtonRightClickAction", "questButtonIndicator", ]); const staleData = !initialQuestDataFetched; const badgeColor = questButtonBadgeColor; const badgeCount = staleData ? 0 : questButtonBadgeCount; const onQuestsPage = staleData ? false : isOnQuestsPage; const displayMode = questButtonDisplay as QuestButtonDisplayMode; const leftClickAction = questButtonLeftClickAction as QuestButtonAction; const middleClickAction = questButtonMiddleClickAction as QuestButtonAction; const rightClickAction = questButtonRightClickAction as QuestButtonAction; const indicatorMode = questButtonIndicator as QuestButtonIndicatorMode; const lowerBadgeProps = getQuestButtonLowerBadgeProps(badgeCount, indicatorMode, badgeColor); const hasUnread = indicatorMode === "pill" || indicatorMode === "both"; const isVisible = showQuestButton(displayMode, badgeCount, onQuestsPage); function handlePress(event: MouseEvent): void { // List items do not support onAuxClick, so middle-click is handled on mousedown. if (event.type === "mousedown" && event.button !== middleClick) { return; } event.preventDefault(); event.stopPropagation(); const action = resolveQuestButtonAction( event, leftClickAction, middleClickAction, rightClickAction, ); if (action === "open-quests") { NavigationRouter.transitionTo(QUEST_PAGE); return; } if (action === "plugin-settings") { openQuestifySettingsModal(plugins.Questify); return; } if (action === "context-menu") { ContextMenuApi.openContextMenu(event, () => ); } } return ( 0 && hasUnread} badgeProps={lowerBadgeProps} onPress={handlePress} /> ); } export interface DummyQuestButtonProps { badgeColor: number | null; leftClickAction: QuestButtonAction; middleClickAction: QuestButtonAction; rightClickAction: QuestButtonAction; showBadge: boolean; showPill: boolean; visible: boolean; } export function DummyQuestButton({ badgeColor, leftClickAction, middleClickAction, rightClickAction, showBadge, showPill, visible, }: DummyQuestButtonProps): JSX.Element { const badgeCount = showBadge ? 3 : 0; const indicatorMode = showBadge ? (showPill ? "both" : "badge") : (showPill ? "pill" : "none"); const lowerBadgeProps = getQuestButtonLowerBadgeProps(badgeCount, indicatorMode, badgeColor); const [dummySelected, setDummySelected] = useState(false); function handlePress(event: MouseEvent): void { // List items do not support onAuxClick, so middle-click is handled on mousedown. if (event.type === "mousedown" && event.button !== middleClick) { return; } event.preventDefault(); event.stopPropagation(); const action = resolveQuestButtonAction( event, leftClickAction, middleClickAction, rightClickAction, ); if (action === "open-quests") { setDummySelected(!dummySelected); return; } if (action === "context-menu") { ContextMenuApi.openContextMenu(event, () => ); } } return ( ); }