From 594445268ca027b8f2b1d9c8edce70208b23c49d Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 14 Mar 2026 22:10:40 -0400 Subject: [PATCH 01/22] fix: weather icon uses wrong time of day for wild --- src/features/pokemon/PokemonPopup.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/features/pokemon/PokemonPopup.jsx b/src/features/pokemon/PokemonPopup.jsx index 4fedc3f44..7a7848472 100644 --- a/src/features/pokemon/PokemonPopup.jsx +++ b/src/features/pokemon/PokemonPopup.jsx @@ -576,9 +576,8 @@ const Info = ({ pokemon, metaData, perms, timeOfDay, backgroundVisuals }) => { const darkMode = useStorage((s) => s.darkMode) const iconStyles = backgroundVisuals?.styles?.icon const hasBackground = Boolean(backgroundVisuals?.hasBackground) - const weatherIconTimeOfDay = hasBackground ? 'night' : timeOfDay - const weatherIconUrl = - Icons?.getWeather?.(weather, weatherIconTimeOfDay) || '' + const weatherIconUrl = Icons?.getWeather?.(weather, timeOfDay) || '' + const weatherIconColor = backgroundVisuals?.primaryColor || '#fff' return ( { maskPosition: 'center', WebkitMaskSize: 'contain', maskSize: 'contain', - backgroundColor: '#fff', + backgroundColor: weatherIconColor, } : { backgroundImage: `url(${weatherIconUrl})`, From 8cbf23e4328aa88c23a4870dab8bf10807670d32 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 15 Mar 2026 03:16:41 +0000 Subject: [PATCH 02/22] chore(release): v1.41.0-develop.12 [skip ci] # [1.41.0-develop.12](https://github.com/WatWowMap/ReactMap/compare/v1.41.0-develop.11...v1.41.0-develop.12) (2026-03-15) ### Bug Fixes * weather icon uses wrong time of day for wild ([5944452](https://github.com/WatWowMap/ReactMap/commit/594445268ca027b8f2b1d9c8edce70208b23c49d)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd5b31f0e..272894066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.41.0-develop.12](https://github.com/WatWowMap/ReactMap/compare/v1.41.0-develop.11...v1.41.0-develop.12) (2026-03-15) + + +### Bug Fixes + +* weather icon uses wrong time of day for wild ([5944452](https://github.com/WatWowMap/ReactMap/commit/594445268ca027b8f2b1d9c8edce70208b23c49d)) + # [1.41.0-develop.11](https://github.com/WatWowMap/ReactMap/compare/v1.41.0-develop.10...v1.41.0-develop.11) (2026-03-15) diff --git a/package.json b/package.json index 7b9764019..c0bf08e97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reactmap", - "version": "1.41.0-develop.11", + "version": "1.41.0-develop.12", "private": true, "description": "React based frontend map.", "license": "MIT", From b77145dd8e037f0916d72c6a84e76fbb9584bde8 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 11 Mar 2026 22:06:33 -0400 Subject: [PATCH 03/22] fix: change scan-on-demand dialog to notification --- src/components/Notification.jsx | 31 ++++++++++++---- src/features/scanner/ScanDialog.jsx | 56 +++++++++++++---------------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/components/Notification.jsx b/src/components/Notification.jsx index 9afd2190d..7c6689881 100644 --- a/src/components/Notification.jsx +++ b/src/components/Notification.jsx @@ -14,6 +14,7 @@ function SlideTransition(props) { /** @type {React.CSSProperties} */ const alertStyle = { textAlign: 'center', color: 'white' } +const DEFAULT_AUTO_HIDE_DURATION = 5000 /** * @@ -26,6 +27,9 @@ const alertStyle = { textAlign: 'center', color: 'white' } * children?: T extends string ? never : React.ReactNode * cb?: () => void * title?: string + * autoHideDuration?: number | null + * ignoreClickaway?: boolean + * closable?: boolean * }} props * @returns */ @@ -37,34 +41,47 @@ export function Notification({ children, cb, title, + autoHideDuration = DEFAULT_AUTO_HIDE_DURATION, + ignoreClickaway = false, + closable = true, }) { const { t } = useTranslation() - const [alert, setAlert] = React.useState(open || false) + const [alert, setAlert] = React.useState(!!open) const handleClose = React.useCallback(() => { setAlert(false) if (cb) cb() }, [cb]) + const handleSnackbarClose = React.useCallback( + (_, reason) => { + if (reason === 'clickaway' && ignoreClickaway) return + if (!closable) return + handleClose() + }, + [closable, handleClose, ignoreClickaway], + ) + React.useEffect(() => { - setAlert(open) + setAlert(!!open) - if (open) { + if (open && typeof autoHideDuration === 'number') { const timer = setTimeout(() => { handleClose() - }, 5000) + }, autoHideDuration) return () => clearTimeout(timer) } - }, [open]) + return undefined + }, [autoHideDuration, handleClose, open]) return ( - /** @type {import('@components/dialogs/Footer').FooterButton[]} */ ([ - { - name: 'close', - icon: 'Clear', - color: 'primary', - align: 'right', - action: handleClose, - }, - ]), - [handleClose], - ) + const resultOpen = scanMode === 'confirmed' || scanMode === 'error' + const resultSeverity = scanMode === 'error' ? 'error' : 'success' return ( - -
- - - {scanMode && t(`scan_${scanMode}`)} - - -
-
+ <> + + {t('scan_loading')} + + + {resultOpen ? t(`scan_${scanMode}`) : null} + + ) } From 8acf245983402e9e14ef78f628fae217c1e23000 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 25 Mar 2026 14:07:45 -0400 Subject: [PATCH 04/22] fix(pokestops): align incident blocker visibility across markers and popups Normalize pokestop incident state into shared marker and popup views so showcases, gold stops, Kecleon, and invasions follow the same precedence rules across rendering paths. Add blocker metadata and showcase expiry to the GraphQL payload, then use that state on the client to: - derive marker-visible vs popup-visible incidents - keep timers and base marker display in sync with filtered incidents - gate showcase range on visible showcase markers - show explicit blocked reasons in the popup for hidden incidents Also add the missing gold stop blocker locale string. --- packages/locales/lib/human/en.json | 1 + packages/types/lib/scanner.d.ts | 2 + server/src/graphql/typeDefs/scanner.graphql | 3 + server/src/models/Pokestop.js | 57 +++- src/features/pokestop/PokestopPopup.jsx | 51 +++- src/features/pokestop/PokestopTile.jsx | 233 +++++++++------ src/features/pokestop/incidentPriority.js | 311 ++++++++++++++++++++ src/features/pokestop/usePokestopMarker.js | 88 ++++-- src/services/queries/pokestop.js | 3 + 9 files changed, 614 insertions(+), 135 deletions(-) create mode 100644 src/features/pokestop/incidentPriority.js diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index b44f66bee..a4ff952f1 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -623,6 +623,7 @@ "show_size_indicator": "Show Size Indicator", "size": "Size", "gold_stop": "Gold Stop", + "gold_stop_block": "Blocked due to a gold stop", "profile_backups": "Profile Swapping", "new_backup": "New Backup", "create": "Create", diff --git a/packages/types/lib/scanner.d.ts b/packages/types/lib/scanner.d.ts index 6cce54a32..e640c8aed 100644 --- a/packages/types/lib/scanner.d.ts +++ b/packages/types/lib/scanner.d.ts @@ -215,6 +215,8 @@ export interface Pokestop { showcase_ranking_standard?: number showcase_rankings?: ShowcaseDetails | string hasShowcase: boolean + incident_blocker_display_type: number | null + incident_blocker_expire_timestamp: number | null } export type FullPokestop = FullModel diff --git a/server/src/graphql/typeDefs/scanner.graphql b/server/src/graphql/typeDefs/scanner.graphql index 12bc0efa2..bc7ae2695 100644 --- a/server/src/graphql/typeDefs/scanner.graphql +++ b/server/src/graphql/typeDefs/scanner.graphql @@ -148,6 +148,9 @@ type Pokestop { power_up_points: Int power_up_end_timestamp: Int hasShowcase: Boolean + showcase_expiry: Int + incident_blocker_display_type: Int + incident_blocker_expire_timestamp: Int } type PokemonShinyStats { diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index 25e12d84a..e7560311d 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -720,6 +720,41 @@ class Pokestop extends Model { fields.forEach((field) => (target[field] = source[field])) } + static getIncidentDisplayType(incident, isMad, hasMultiInvasions) { + return isMad && !hasMultiInvasions + ? MAD_GRUNT_MAP[incident.grunt_type] || 8 + : incident.display_type + } + + static getIncidentBlocker(incidents, isMad, hasMultiInvasions) { + const blocker = { + displayType: 0, + expireTimestamp: 0, + } + + ;(incidents || []).forEach((incident) => { + const displayType = this.getIncidentDisplayType( + incident, + isMad, + hasMultiInvasions, + ) + // Showcase expiry is tracked separately on the client so local timer + // updates can fall through to the next hidden blocker without a refetch. + if ( + displayType === 7 && + incident.incident_expire_timestamp > blocker.expireTimestamp + ) { + blocker.displayType = 7 + blocker.expireTimestamp = incident.incident_expire_timestamp + } + }) + + return { + displayType: blocker.displayType || null, + expireTimestamp: blocker.expireTimestamp || null, + } + } + // filters and removes unwanted data static secondaryFilter( queryResults, @@ -734,7 +769,18 @@ class Pokestop extends Model { const filteredResults = [] for (let i = 0; i < queryResults.length; i += 1) { const pokestop = queryResults[i] - const filtered = { hasShowcase: pokestop.showcase_expiry > ts } + const hasShowcase = pokestop.showcase_expiry > ts + const incidentBlocker = this.getIncidentBlocker( + pokestop.invasions, + isMad, + hasMultiInvasions, + ) + const filtered = { + hasShowcase, + showcase_expiry: pokestop.showcase_expiry || null, + incident_blocker_display_type: incidentBlocker.displayType, + incident_blocker_expire_timestamp: incidentBlocker.expireTimestamp, + } this.fieldAssigner(filtered, pokestop, [ 'id', @@ -790,10 +836,11 @@ class Pokestop extends Model { event.display_type === 9 ? pokestop.showcase_ranking_standard : null, - display_type: - isMad && !hasMultiInvasions - ? MAD_GRUNT_MAP[event.grunt_type] || 8 - : event.display_type, + display_type: this.getIncidentDisplayType( + event, + isMad, + hasMultiInvasions, + ), })) .filter((event) => event.showcase_pokemon_id diff --git a/src/features/pokestop/PokestopPopup.jsx b/src/features/pokestop/PokestopPopup.jsx index 0f5e5129b..741990277 100644 --- a/src/features/pokestop/PokestopPopup.jsx +++ b/src/features/pokestop/PokestopPopup.jsx @@ -40,6 +40,13 @@ import { usePokemonBackgroundVisual, } from '@hooks/usePokemonBackgroundVisuals' import { resolveShowcaseEventIcon } from './resolveShowcaseEventIcon' +import { + INCIDENT_DISPLAY_TYPES, + getEventIncidentPriority, + getIncidentBlockReason, + getInvasionIncidentPriority, + isIncidentBlockedBy, +} from './incidentPriority' /** * @@ -48,6 +55,9 @@ import { resolveShowcaseEventIcon } from './resolveShowcaseEventIcon' * hasInvasion: boolean * hasQuest: boolean * hasEvent: boolean + * popupInvasions: import('@rm/types').Invasion[] + * popupEvents: import('@rm/types').Event[] + * incidentBlocker: { event: { display_type?: number | string | null }, priority: number } | null * }} props * @returns */ @@ -56,11 +66,15 @@ export function PokestopPopup({ hasInvasion, hasQuest, hasEvent, + popupInvasions, + popupEvents, + incidentBlocker, ...pokestop }) { const { t } = useTranslation() const Icons = useMemory((s) => s.Icons) - const { lure_expire_timestamp, lure_id, invasions, events } = pokestop + const { lure_expire_timestamp, lure_id } = pokestop + const incidentBlockReason = getIncidentBlockReason(incidentBlocker) useAnalytics( 'Popup', @@ -166,7 +180,7 @@ export function PokestopPopup({ )} {hasInvasion && ( <> - {invasions.map((invasion, index) => ( + {popupInvasions.map((invasion, index) => ( @@ -180,7 +194,14 @@ export function PokestopPopup({ invasion.grunt_type, invasion.confirmed, )} - disabled={pokestop.hasShowcase ? 'showcase_block' : ''} + disabled={ + isIncidentBlockedBy( + incidentBlocker, + getInvasionIncidentPriority(invasion), + ) + ? incidentBlockReason + : '' + } tt={ invasion.grunt_type === 44 && !invasion.confirmed ? [`grunt_a_${invasion.grunt_type}`, ' / ', 'decoy'] @@ -198,11 +219,12 @@ export function PokestopPopup({ {(hasQuest || hasLure || hasInvasion) && ( )} - {events.map(({ showcase_rankings, ...event }, index) => { + {popupEvents.map(({ showcase_rankings, ...event }, index) => { + const displayType = Number(event.display_type ?? 0) const { contest_entries = [], ...showcase } = showcase_rankings || { contest_entries: [] } const showcaseIcon = - event.display_type === 9 + displayType === INCIDENT_DISPLAY_TYPES.SHOWCASE ? resolveShowcaseEventIcon(event, Icons) : null return ( @@ -215,13 +237,16 @@ export function PokestopPopup({ ) : showcaseIcon ? ( showcaseIcon.url ) : ( - Icons.getEventStops(event.display_type) + Icons.getEventStops(displayType) ) } tt={t( - `display_type_${event.display_type}`, + `display_type_${displayType}`, t('unknown_event'), )} > diff --git a/src/features/pokestop/PokestopTile.jsx b/src/features/pokestop/PokestopTile.jsx index 8e64e49a7..63e48034f 100644 --- a/src/features/pokestop/PokestopTile.jsx +++ b/src/features/pokestop/PokestopTile.jsx @@ -12,6 +12,10 @@ import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { TooltipWrapper } from '@components/ToolTipWrapper' import { PokestopPopup } from './PokestopPopup' +import { + INCIDENT_DISPLAY_TYPES, + getPokestopIncidentState, +} from './incidentPriority' import { usePokestopMarker } from './usePokestopMarker' /** @@ -22,6 +26,16 @@ import { usePokestopMarker } from './usePokestopMarker' const BasePokestopTile = (pokestop) => { const [stateChange, setStateChange] = React.useState(false) const [markerRef, setMarkerRef] = React.useState(null) + const ts = Date.now() / 1000 + const incidentState = getPokestopIncidentState({ + events: pokestop.events, + invasions: pokestop.invasions, + showcase_expiry: pokestop.showcase_expiry, + incident_blocker_display_type: pokestop.incident_blocker_display_type, + incident_blocker_expire_timestamp: + pokestop.incident_blocker_expire_timestamp, + ts, + }) const hasRoutes = useRouteStore( React.useCallback( (state) => @@ -37,111 +51,143 @@ const BasePokestopTile = (pokestop) => { const selectPoi = useRouteStore((s) => s.selectPoi) const [ - hasLure, - hasInvasion, - hasQuest, - hasEvent, - hasAllStops, - showTimer, + canShowLures, + canShowInvasions, + canShowQuests, + canShowEvents, + canShowPokestops, + hasTimerOverride, interactionRangeZoom, - hasShowcase, - ] = useMemory((s) => { - const newTs = Date.now() / 1000 - const { filters } = useStorage.getState() - const { - config, - timerList, - auth: { perms }, - } = s - return [ - pokestop.lure_expire_timestamp > newTs && perms.lures, - !!( - perms.invasions && - pokestop.invasions?.some( - (invasion) => - invasion.grunt_type && invasion.incident_expire_timestamp > newTs, - ) - ), - !!(perms.quests && pokestop.quests?.length), - !!( - perms.eventStops && - filters.pokestops.eventStops && - pokestop.events?.some((event) => event.event_expire_timestamp > newTs) - ), - (filters.pokestops.allPokestops || pokestop.ar_scan_eligible) && - perms.pokestops, - timerList.includes(pokestop.id), - config.general.interactionRangeZoom, - !!(perms.pokestops && pokestop.hasShowcase), - ] - }, basicEqualFn) + ] = useMemory( + (s) => [ + !!s.auth.perms.lures, + !!s.auth.perms.invasions, + !!s.auth.perms.quests, + !!s.auth.perms.eventStops, + !!s.auth.perms.pokestops, + s.timerList.includes(pokestop.id), + s.config.general.interactionRangeZoom, + ], + basicEqualFn, + ) const [ - invasionTimers, - lureTimers, - eventStopTimers, - lureRange, - showcaseRange, - interactionRange, + showEventStops, + showAllStops, + showInvasionTimers, + showLureTimers, + showEventStopTimers, + showLureRange, + showShowcaseRange, + showInteractionRange, customRange, + zoom, ] = useStorage((s) => { - const { userSettings, zoom } = s + const { userSettings } = s return [ - userSettings.pokestops.invasionTimers || showTimer, - userSettings.pokestops.lureTimers || showTimer, - userSettings.pokestops.eventStopTimers || showTimer, - !!userSettings.pokestops.lureRange && zoom >= interactionRangeZoom, - !!userSettings.pokestops.showcaseRange && - zoom >= interactionRangeZoom && - hasShowcase, - !!userSettings.pokestops.interactionRanges && - zoom >= interactionRangeZoom, - zoom >= interactionRangeZoom - ? +userSettings.pokestops.customRange || 0 - : 0, + !!s.filters.pokestops.eventStops, + !!s.filters.pokestops.allPokestops, + !!(userSettings.pokestops.invasionTimers || hasTimerOverride), + !!(userSettings.pokestops.lureTimers || hasTimerOverride), + !!(userSettings.pokestops.eventStopTimers || hasTimerOverride), + !!userSettings.pokestops.lureRange, + !!userSettings.pokestops.showcaseRange, + !!userSettings.pokestops.interactionRanges, + +userSettings.pokestops.customRange || 0, + s.zoom, ] }, basicEqualFn) - const timers = React.useMemo(() => { - const internalTimers = /** @type {number[]} */ ([]) - if (invasionTimers && hasInvasion) { - pokestop.invasions.forEach((invasion) => - internalTimers.push(invasion.incident_expire_timestamp), + const hasLure = pokestop.lure_expire_timestamp > ts && canShowLures + const hasQuest = !!(canShowQuests && pokestop.quests?.length) + const hasInvasion = !!( + canShowInvasions && incidentState.popupInvasions.length + ) + const hasEvent = !!( + canShowEvents && + showEventStops && + incidentState.popupEvents.length + ) + const visibleMarkerInvasions = canShowInvasions + ? incidentState.markerInvasions + : [] + const visibleMarkerEvents = + canShowEvents && showEventStops ? incidentState.markerEvents : [] + const hasVisibleInvasion = !!( + canShowInvasions && visibleMarkerInvasions.length + ) + const hasVisibleEvent = !!visibleMarkerEvents.length + const hasVisibleShowcase = visibleMarkerEvents.some( + (event) => + Number(event.display_type ?? 0) === INCIDENT_DISPLAY_TYPES.SHOWCASE, + ) + const hasAllStops = !!( + (showAllStops || pokestop.ar_scan_eligible) && + canShowPokestops + ) + const withinRangeZoom = zoom >= interactionRangeZoom + const lureRange = showLureRange && withinRangeZoom + const showcaseRange = + showShowcaseRange && withinRangeZoom && hasVisibleShowcase + const interactionRange = showInteractionRange && withinRangeZoom + const renderedCustomRange = withinRangeZoom ? customRange : 0 + + const [refreshTimers, tooltipTimers] = React.useMemo(() => { + const internalRefreshTimers = [...incidentState.expiryTimestamps] + const internalTooltipTimers = /** @type {number[]} */ ([]) + + if (showInvasionTimers && hasVisibleInvasion) { + visibleMarkerInvasions.forEach((invasion) => + internalTooltipTimers.push(invasion.incident_expire_timestamp), ) } - if (lureTimers && hasLure) { - internalTimers.push(pokestop.lure_expire_timestamp) + if (showLureTimers && hasLure) { + internalRefreshTimers.push(pokestop.lure_expire_timestamp) + internalTooltipTimers.push(pokestop.lure_expire_timestamp) } - if (eventStopTimers && hasEvent) { - pokestop.events.forEach((event) => { - internalTimers.push(event.event_expire_timestamp) + if (showEventStopTimers && hasVisibleEvent) { + visibleMarkerEvents.forEach((event) => { + internalTooltipTimers.push(event.event_expire_timestamp) }) } - return internalTimers + + return [internalRefreshTimers, internalTooltipTimers] }, [ - invasionTimers, - hasInvasion, - lureTimers, + incidentState.expiryTimestamps, + visibleMarkerEvents, + visibleMarkerInvasions, + showInvasionTimers, + hasVisibleInvasion, + showLureTimers, hasLure, - eventStopTimers, - hasEvent, + showEventStopTimers, + hasVisibleEvent, + pokestop.lure_expire_timestamp, ]) useForcePopup(pokestop.id, markerRef) - useMarkerTimer(timers.length ? Math.min(...timers) : null, markerRef, () => - setStateChange(!stateChange), + useMarkerTimer( + refreshTimers.length ? Math.min(...refreshTimers) : null, + markerRef, + () => setStateChange(!stateChange), ) const handlePopupOpen = useManualPopupTracker('pokestops', pokestop.id) const icon = usePokestopMarker({ hasQuest, hasLure, - hasInvasion, - hasEvent, + markerEvents: visibleMarkerEvents, + markerInvasions: visibleMarkerInvasions, + baseIncidentDisplay: + canShowEvents && showEventStops ? incidentState.baseDisplay : '', ...pokestop, }) - return hasQuest || hasLure || hasInvasion || hasEvent || hasAllStops ? ( + return hasQuest || + hasLure || + hasVisibleInvasion || + hasVisibleEvent || + hasAllStops ? ( { hasInvasion={hasInvasion} hasQuest={hasQuest} hasEvent={hasEvent} + popupInvasions={incidentState.popupInvasions} + popupEvents={incidentState.popupEvents} + incidentBlocker={incidentState.blocker} {...pokestop} /> - {Boolean(timers.length) && ( - + {Boolean(tooltipTimers.length) && ( + )} {interactionRange && ( { pathOptions={{ color: '#39a18f', weight: 1 }} /> )} - {!!customRange && ( + {!!renderedCustomRange && ( )} @@ -206,17 +255,31 @@ export const PokestopTile = React.memo( prev.lure_expire_timestamp === next.lure_expire_timestamp && prev.updated === next.updated && prev.hasShowcase === next.hasShowcase && + prev.showcase_expiry === next.showcase_expiry && + prev.incident_blocker_display_type === next.incident_blocker_display_type && + prev.incident_blocker_expire_timestamp === + next.incident_blocker_expire_timestamp && prev.quests?.length === next.quests?.length && (prev.quests && next.quests ? prev.quests.every((q, i) => q.with_ar === next.quests[i]?.with_ar) : true) && prev.invasions?.length === next.invasions?.length && (prev.invasions && next.invasions - ? prev.invasions?.every( + ? prev.invasions.every( (inv, i) => - inv.confirmed === next?.invasions?.[i]?.confirmed && - inv.grunt_type === next?.invasions?.[i]?.grunt_type, + inv.confirmed === next.invasions?.[i]?.confirmed && + inv.grunt_type === next.invasions?.[i]?.grunt_type && + inv.incident_expire_timestamp === + next.invasions?.[i]?.incident_expire_timestamp, ) : true) && - prev.events?.length === next.events?.length, + prev.events?.length === next.events?.length && + (prev.events && next.events + ? prev.events.every( + (event, i) => + event.display_type === next.events?.[i]?.display_type && + event.event_expire_timestamp === + next.events?.[i]?.event_expire_timestamp, + ) + : true), ) diff --git a/src/features/pokestop/incidentPriority.js b/src/features/pokestop/incidentPriority.js new file mode 100644 index 000000000..137bc3117 --- /dev/null +++ b/src/features/pokestop/incidentPriority.js @@ -0,0 +1,311 @@ +// @ts-check + +// Larger values are stronger display precedence, matching scanner incident priorities. +export const INCIDENT_PRIORITY_SETTINGS = Object.freeze({ + INCIDENT_CONTEST: 7, + INVASION_GENERIC: 6, + INVASION_GIOVANNI: 5, + INVASION_LEADER: 4, + INVASION_GRUNT: 3, + INVASION_EVENT_NPC: 2, + INCIDENT_POKESTOP_ENCOUNTER: 1, +}) + +export const INCIDENT_DISPLAY_TYPES = Object.freeze({ + GOLD_STOP: 7, + KECLEON: 8, + SHOWCASE: 9, +}) + +/** + * @param {{ display_type?: number | string | null }} event + * @returns {number} + */ +export function getEventIncidentPriority(event) { + switch (Number(event?.display_type ?? 0)) { + case INCIDENT_DISPLAY_TYPES.SHOWCASE: + return INCIDENT_PRIORITY_SETTINGS.INCIDENT_CONTEST + case INCIDENT_DISPLAY_TYPES.GOLD_STOP: + return INCIDENT_PRIORITY_SETTINGS.INVASION_GENERIC + case INCIDENT_DISPLAY_TYPES.KECLEON: + return INCIDENT_PRIORITY_SETTINGS.INCIDENT_POKESTOP_ENCOUNTER + default: + return event?.display_type + ? INCIDENT_PRIORITY_SETTINGS.INVASION_EVENT_NPC + : 0 + } +} + +/** + * @param {{ grunt_type?: number | string | null }} invasion + * @returns {number} + */ +export function getInvasionIncidentPriority(invasion) { + const gruntType = Number(invasion?.grunt_type ?? 0) + if (!gruntType) return 0 + if (gruntType === 44) { + return INCIDENT_PRIORITY_SETTINGS.INVASION_GIOVANNI + } + if (gruntType >= 41 && gruntType <= 43) { + return INCIDENT_PRIORITY_SETTINGS.INVASION_LEADER + } + return INCIDENT_PRIORITY_SETTINGS.INVASION_GRUNT +} + +/** + * @param {{ display_type?: number | string | null, event_expire_timestamp?: number | string | null }} event + * @returns {boolean} + */ +export function isActiveEvent(event, ts) { + return Number(event?.event_expire_timestamp ?? 0) > ts +} + +/** + * @param {{ grunt_type?: number | string | null, incident_expire_timestamp?: number | string | null }} invasion + * @returns {boolean} + */ +export function isActiveInvasion(invasion, ts) { + return ( + Number(invasion?.grunt_type ?? 0) > 0 && + Number(invasion?.incident_expire_timestamp ?? 0) > ts + ) +} + +/** + * @param {{ event: { display_type?: number | string | null }, priority: number, expireTimestamp?: number | null } | null} current + * @param {{ event: { display_type?: number | string | null }, priority: number, expireTimestamp?: number | null } | null} next + * @returns {{ event: { display_type?: number | string | null }, priority: number, expireTimestamp?: number | null } | null} + */ +export function getStrongerIncidentBlocker(current, next) { + if (!current) return next + if (!next) return current + if (next.priority !== current.priority) { + return next.priority > current.priority ? next : current + } + if ( + Number(next.event.display_type ?? 0) !== + Number(current.event.display_type ?? 0) + ) { + return Number(next.event.display_type ?? 0) > + Number(current.event.display_type ?? 0) + ? next + : current + } + return Number(next.expireTimestamp ?? 0) > + Number(current.expireTimestamp ?? 0) + ? next + : current +} + +/** + * @param {{ + * events?: Array<{ display_type?: number | string | null, event_expire_timestamp?: number | string | null }> + * }} param0 + * @returns {{ event: { display_type?: number | string | null }, priority: number, expireTimestamp?: number | null } | null} + */ +export function getVisibleIncidentBlocker({ events = [] } = {}) { + return events.reduce((strongestEvent, event) => { + const priority = getEventIncidentPriority(event) + if (priority < INCIDENT_PRIORITY_SETTINGS.INVASION_GENERIC) { + return strongestEvent + } + return getStrongerIncidentBlocker(strongestEvent, { + event, + priority, + expireTimestamp: Number(event.event_expire_timestamp ?? 0) || null, + }) + }, null) +} + +/** + * @param {{ + * showcase_expiry?: number | string | null + * ts: number + * }} param0 + * @returns {{ event: { display_type?: number | string | null }, priority: number, expireTimestamp?: number | null } | null} + */ +export function getShowcaseIncidentBlocker({ showcase_expiry, ts } = {}) { + const showcaseExpiry = Number(showcase_expiry ?? 0) || null + if (!showcaseExpiry || showcaseExpiry <= ts) { + return null + } + + const showcaseEvent = { display_type: INCIDENT_DISPLAY_TYPES.SHOWCASE } + return { + event: showcaseEvent, + priority: getEventIncidentPriority(showcaseEvent), + expireTimestamp: showcaseExpiry, + } +} + +/** + * @param {{ + * incident_blocker_display_type?: number | string | null + * incident_blocker_expire_timestamp?: number | string | null + * ts: number + * }} param0 + * @returns {{ event: { display_type?: number | string | null }, priority: number, expireTimestamp?: number | null } | null} + */ +export function getFallbackIncidentBlocker({ + incident_blocker_display_type, + incident_blocker_expire_timestamp, + ts, +} = {}) { + const fallbackDisplayType = Number(incident_blocker_display_type ?? 0) || 0 + const fallbackExpireTimestamp = + Number(incident_blocker_expire_timestamp ?? 0) || null + + if ( + (fallbackDisplayType !== INCIDENT_DISPLAY_TYPES.GOLD_STOP && + fallbackDisplayType !== INCIDENT_DISPLAY_TYPES.SHOWCASE) || + !fallbackExpireTimestamp || + fallbackExpireTimestamp <= ts + ) { + return null + } + + const fallbackEvent = { display_type: fallbackDisplayType } + return { + event: fallbackEvent, + priority: getEventIncidentPriority(fallbackEvent), + expireTimestamp: fallbackExpireTimestamp, + } +} + +/** + * @param {{ + * events?: Array<{ display_type?: number | string | null, event_expire_timestamp?: number | string | null }> + * invasions?: Array<{ grunt_type?: number | string | null, incident_expire_timestamp?: number | string | null }> + * showcase_expiry?: number | string | null + * incident_blocker_display_type?: number | string | null + * incident_blocker_expire_timestamp?: number | string | null + * ts: number + * }} param0 + */ +export function getPokestopIncidentState({ + events, + invasions, + showcase_expiry, + incident_blocker_display_type, + incident_blocker_expire_timestamp, + ts, +} = {}) { + const normalizedEvents = Array.isArray(events) ? events : [] + const normalizedInvasions = Array.isArray(invasions) ? invasions : [] + const popupEvents = normalizedEvents.filter((event) => + isActiveEvent(event, ts), + ) + const popupInvasions = normalizedInvasions.filter((invasion) => + isActiveInvasion(invasion, ts), + ) + const visibleBlocker = getVisibleIncidentBlocker({ + events: popupEvents, + }) + const showcaseBlocker = getShowcaseIncidentBlocker({ + showcase_expiry, + ts, + }) + const fallbackBlocker = getFallbackIncidentBlocker({ + incident_blocker_display_type, + incident_blocker_expire_timestamp, + ts, + }) + const blocker = getStrongerIncidentBlocker( + getStrongerIncidentBlocker(visibleBlocker, showcaseBlocker), + fallbackBlocker, + ) + const markerEvents = blocker + ? popupEvents.filter( + (event) => getEventIncidentPriority(event) >= blocker.priority, + ) + : popupEvents + const markerInvasions = blocker ? [] : popupInvasions + const baseDisplay = getBasePokestopIncidentDisplay({ + events: markerEvents, + invasions: markerInvasions, + }) + const expiryTimestamps = [ + ...new Set( + [ + ...popupInvasions.map((invasion) => + Number(invasion.incident_expire_timestamp ?? 0), + ), + ...popupEvents.map((event) => + Number(event.event_expire_timestamp ?? 0), + ), + Number(blocker?.expireTimestamp ?? 0), + ].filter(Boolean), + ), + ] + + return { + blocker, + popupEvents, + popupInvasions, + markerEvents, + markerInvasions, + baseDisplay, + expiryTimestamps, + } +} + +/** + * @param {{ + * events?: Array<{ display_type?: number | string | null }> + * invasions?: Array<{ grunt_type?: number | string | null }> + * }} param0 + * @returns {number | string} + */ +export function getBasePokestopIncidentDisplay({ + events = [], + invasions = [], +} = {}) { + const strongestVisibleEvent = events.reduce((strongest, event) => { + if (!strongest) return event + const strongestPriority = getEventIncidentPriority(strongest) + const priority = getEventIncidentPriority(event) + if (priority !== strongestPriority) { + return priority > strongestPriority ? event : strongest + } + return Number(event.display_type ?? 0) > Number(strongest.display_type ?? 0) + ? event + : strongest + }, null) + + const strongestEventPriority = strongestVisibleEvent + ? getEventIncidentPriority(strongestVisibleEvent) + : 0 + const strongestInvasionPriority = invasions.reduce( + (maxPriority, invasion) => + Math.max(maxPriority, getInvasionIncidentPriority(invasion)), + 0, + ) + + return strongestEventPriority > strongestInvasionPriority + ? Number(strongestVisibleEvent?.display_type ?? 0) || '' + : '' +} + +/** + * @param {{ priority: number } | null} blocker + * @returns {string} + */ +export function getIncidentBlockReason(blocker) { + switch (blocker?.priority) { + case INCIDENT_PRIORITY_SETTINGS.INCIDENT_CONTEST: + return 'showcase_block' + case INCIDENT_PRIORITY_SETTINGS.INVASION_GENERIC: + return 'gold_stop_block' + default: + return '' + } +} + +/** + * @param {{ priority: number } | null} blocker + * @param {number} priority + * @returns {boolean} + */ +export function isIncidentBlockedBy(blocker, priority) { + return !!blocker && blocker.priority > priority +} diff --git a/src/features/pokestop/usePokestopMarker.js b/src/features/pokestop/usePokestopMarker.js index 5ddcebd88..4517d6db5 100644 --- a/src/features/pokestop/usePokestopMarker.js +++ b/src/features/pokestop/usePokestopMarker.js @@ -4,6 +4,7 @@ import { divIcon } from 'leaflet' import { basicEqualFn, useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { useOpacity } from '@hooks/useOpacity' +import { INCIDENT_DISPLAY_TYPES } from './incidentPriority' import { resolveShowcaseEventIcon } from './resolveShowcaseEventIcon' /** @@ -11,23 +12,22 @@ import { resolveShowcaseEventIcon } from './resolveShowcaseEventIcon' * @param {{ * hasQuest: boolean, * hasLure: boolean, - * hasInvasion: boolean, - * hasEvent: boolean, + * markerEvents: Array<{ display_type?: number | string | null }>, + * markerInvasions: Array, + * baseIncidentDisplay: number | string, * } & import('@rm/types').Pokestop} param0 * @returns */ export function usePokestopMarker({ hasQuest, hasLure, - hasInvasion, - hasEvent, lure_id, ar_scan_eligible, power_up_level, - events, - invasions, quests, - hasShowcase, + markerEvents, + markerInvasions, + baseIncidentDisplay, }) { const [, Icons, masterfile] = useStorage( (s) => [ @@ -38,6 +38,18 @@ export function usePokestopMarker({ (a, b) => Object.entries(a[0]).every(([k, v]) => b[0][k] === v), ) + const hasVisibleInvasion = markerInvasions.some( + (invasion) => !!invasion.grunt_type, + ) + const shouldShowStandaloneKecleonBadge = + !hasQuest && + !hasVisibleInvasion && + markerEvents.length > 0 && + markerEvents.every( + (event) => + Number(event.display_type ?? 0) === INCIDENT_DISPLAY_TYPES.KECLEON, + ) + const getOpacity = useOpacity('pokestops', 'invasion') const [ showArBadge, @@ -54,11 +66,11 @@ export function usePokestopMarker({ pokestops.showNoArQuestDotBadge ?? true, Icons.getPokestops( hasLure ? lure_id : 0, - hasInvasion, + hasVisibleInvasion, hasQuest && pokestops.hasQuestIndicator, ar_scan_eligible && (pokestops.showArBadge || !!power_up_level), power_up_level, - hasEvent ? Math.max(...events.map((event) => event.display_type)) : 0, + baseIncidentDisplay, ), hasLure ? Icons.getSize( @@ -85,12 +97,11 @@ export function usePokestopMarker({ const invasionSizes = [] const questIcons = [] const questSizes = [] - const showcaseIcons = [] - const showcaseSizes = [] - const canShowInvasionIcons = hasInvasion && !hasShowcase + const eventIcons = [] + const eventSizes = [] - if (canShowInvasionIcons) { - invasions.forEach((invasion) => { + if (hasVisibleInvasion) { + markerInvasions.forEach((invasion) => { if (invasion.grunt_type) { invasionIcons.unshift({ icon: Icons.getInvasions(invasion.grunt_type, invasion.confirmed), @@ -254,7 +265,7 @@ export function usePokestopMarker({ }) } - if (hasQuest && !(hasInvasion && invasionMod?.removeQuest)) { + if (hasQuest && !(hasVisibleInvasion && invasionMod?.removeQuest)) { quests.forEach((quest) => { const { quest_item_id, @@ -365,25 +376,38 @@ export function usePokestopMarker({ popupY += rewardMod.popupY }) } - if (hasEvent && !canShowInvasionIcons && !hasQuest) { - events.forEach((event) => { - if (event.display_type === 8) { - // Only show Kecleon if there's no active showcase blocking it - if (!hasShowcase) { - showcaseIcons.unshift({ - url: Icons.getPokemon(352), - }) - showcaseSizes.unshift(Icons.getSize('event', filters.b7?.size)) + if (markerEvents.length && !hasQuest) { + markerEvents.forEach((event) => { + const displayType = Number(event.display_type ?? 0) + if (displayType === INCIDENT_DISPLAY_TYPES.KECLEON) { + if (!shouldShowStandaloneKecleonBadge || eventIcons.length) { + return } - } else if (event.display_type === 9) { + eventIcons.unshift({ + url: Icons.getPokemon(352), + }) + eventSizes.unshift( + Icons.getSize( + 'event', + filters[`b${INCIDENT_DISPLAY_TYPES.KECLEON}`]?.size, + ), + ) + } else if (displayType === INCIDENT_DISPLAY_TYPES.SHOWCASE) { const showcaseIcon = resolveShowcaseEventIcon(event, Icons) - showcaseIcons.unshift({ + eventIcons.unshift({ url: showcaseIcon.url, decoration: showcaseIcon.decoration, }) - showcaseSizes.unshift( + eventSizes.unshift( Icons.getSize('event', filters[showcaseIcon.sizeFilterKey]?.size), ) + } else { + eventIcons.unshift({ + url: Icons.getEventStops(displayType), + }) + eventSizes.unshift( + Icons.getSize('event', filters[`b${displayType}`]?.size), + ) } popupYOffset += eventMod.offsetY - 1 popupX += eventMod.popupX @@ -392,7 +416,9 @@ export function usePokestopMarker({ } const totalQuestSize = questSizes.reduce((a, b) => a + b, 0) const totalInvasionSize = invasionSizes.reduce((a, b) => a + b, 0) - const totalShowcaseSize = showcaseSizes.reduce((a, b) => a + b, -3) + const totalEventSize = eventSizes.length + ? eventSizes.reduce((a, b) => a + b, -3) + : 0 const showAr = showArBadge && ar_scan_eligible && !baseIcon.includes('_ar') @@ -421,11 +447,11 @@ export function usePokestopMarker({ }) }) - showcaseIcons.forEach((icon, index) => { + eventIcons.forEach((icon, index) => { stackItems.push({ type: 'event', url: icon.url, - size: showcaseSizes[index], + size: eventSizes[index], modifier: eventMod, decoration: icon.decoration, }) @@ -539,7 +565,7 @@ export function usePokestopMarker({ ? pokestopMod.manualPopup - totalInvasionSize * 0.25 - totalQuestSize * 0.1 - : -(baseSize + totalInvasionSize + totalQuestSize + totalShowcaseSize) / + : -(baseSize + totalInvasionSize + totalQuestSize + totalEventSize) / popupYOffset) + popupY, ], className: 'pokestop-marker', diff --git a/src/services/queries/pokestop.js b/src/services/queries/pokestop.js index 334531d78..ba9914a22 100644 --- a/src/services/queries/pokestop.js +++ b/src/services/queries/pokestop.js @@ -16,6 +16,9 @@ const core = gql` power_up_points power_up_end_timestamp hasShowcase + showcase_expiry + incident_blocker_display_type + incident_blocker_expire_timestamp } ` From 13eced03c14dd2d11f3062abb03ed98a0eeb51da Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 26 Mar 2026 02:25:06 +0000 Subject: [PATCH 05/22] chore(release): v1.41.2-develop.1 [skip ci] ## [1.41.2-develop.1](https://github.com/WatWowMap/ReactMap/compare/v1.41.1...v1.41.2-develop.1) (2026-03-26) ### Bug Fixes * change scan-on-demand dialog to notification ([b77145d](https://github.com/WatWowMap/ReactMap/commit/b77145dd8e037f0916d72c6a84e76fbb9584bde8)) * **pokestops:** align incident blocker visibility across markers and popups ([8acf245](https://github.com/WatWowMap/ReactMap/commit/8acf245983402e9e14ef78f628fae217c1e23000)) * weather icon uses wrong time of day for wild ([5944452](https://github.com/WatWowMap/ReactMap/commit/594445268ca027b8f2b1d9c8edce70208b23c49d)) --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a9cb6f6..8e0178751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [1.41.2-develop.1](https://github.com/WatWowMap/ReactMap/compare/v1.41.1...v1.41.2-develop.1) (2026-03-26) + + +### Bug Fixes + +* change scan-on-demand dialog to notification ([b77145d](https://github.com/WatWowMap/ReactMap/commit/b77145dd8e037f0916d72c6a84e76fbb9584bde8)) +* **pokestops:** align incident blocker visibility across markers and popups ([8acf245](https://github.com/WatWowMap/ReactMap/commit/8acf245983402e9e14ef78f628fae217c1e23000)) +* weather icon uses wrong time of day for wild ([5944452](https://github.com/WatWowMap/ReactMap/commit/594445268ca027b8f2b1d9c8edce70208b23c49d)) + ## [1.41.1](https://github.com/WatWowMap/ReactMap/compare/v1.41.0...v1.41.1) (2026-03-15) diff --git a/package.json b/package.json index 0c4bac6ff..a3f9687fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reactmap", - "version": "1.41.1", + "version": "1.41.2-develop.1", "private": true, "description": "React based frontend map.", "license": "MIT", From 8dfbf6d6cdc484e5555d30f58290f6e88baee308 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:41:44 -0400 Subject: [PATCH 06/22] chore(deps): bump flatted from 3.3.3 to 3.4.2 (#1196) Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2. - [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2) --- updated-dependencies: - dependency-name: flatted dependency-version: 3.4.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mygod --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fff51ece0..cefd17c3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4788,9 +4788,9 @@ flat-cache@^3.0.4: rimraf "^3.0.2" flatted@^3.2.9: - version "3.3.3" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" - integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" From 10d0316d371a23430f3441769858cdbcff7642d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:55:24 -0700 Subject: [PATCH 07/22] chore(deps): bump handlebars from 4.7.8 to 4.7.9 (#1198) Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9. - [Release notes](https://github.com/handlebars-lang/handlebars.js/releases) - [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md) - [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9) --- updated-dependencies: - dependency-name: handlebars dependency-version: 4.7.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cefd17c3a..743c4efe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5246,9 +5246,9 @@ graphql@16.11.0: integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw== handlebars@^4.7.7: - version "4.7.8" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" - integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + version "4.7.9" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.9.tgz#6f139082ab58dc4e5a0e51efe7db5ae890d56a0f" + integrity sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ== dependencies: minimist "^1.2.5" neo-async "^2.6.2" From 184eed6d0702647bfe8d8d7d16f2901244385abd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:37:40 -0700 Subject: [PATCH 08/22] chore(deps): bump handlebars from 4.7.8 to 4.7.9 (#1199) Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9. - [Release notes](https://github.com/handlebars-lang/handlebars.js/releases) - [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md) - [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9) --- updated-dependencies: - dependency-name: handlebars dependency-version: 4.7.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From b3004d54a856e53cdf8109b898e4f947d0c52c29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:02:00 -0400 Subject: [PATCH 09/22] chore(deps): bump @apollo/server from 5.4.0 to 5.5.0 (#1197) Bumps [@apollo/server](https://github.com/apollographql/apollo-server/tree/HEAD/packages/server) from 5.4.0 to 5.5.0. - [Release notes](https://github.com/apollographql/apollo-server/releases) - [Changelog](https://github.com/apollographql/apollo-server/blob/main/packages/server/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-server/commits/@apollo/server@5.5.0/packages/server) --- updated-dependencies: - dependency-name: "@apollo/server" dependency-version: 5.5.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a3f9687fd..7c2209673 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ }, "dependencies": { "@apollo/client": "3.11.4", - "@apollo/server": "5.4.0", + "@apollo/server": "5.5.0", "@as-integrations/express5": "1.1.2", "@emotion/react": "11.14.0", "@emotion/styled": "11.13.0", diff --git a/yarn.lock b/yarn.lock index 743c4efe1..39217b3ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,10 +55,10 @@ "@apollo/utils.keyvaluecache" "^4.0.0" "@apollo/utils.logger" "^3.0.0" -"@apollo/server@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@apollo/server/-/server-5.4.0.tgz#ad161a6e8b14f5227027205e0970a91667351e49" - integrity sha512-E0/2C5Rqp7bWCjaDh4NzYuEPDZ+dltTf2c0FI6GCKJA6GBetVferX3h1//1rS4+NxD36wrJsGGJK+xyT/M3ysg== +"@apollo/server@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@apollo/server/-/server-5.5.0.tgz#7918e45af53879b11baea04772fc2968fe64492c" + integrity sha512-vWtodBOK/SZwBTJzItECOmLfL8E8pn/IdvP7pnxN5g2tny9iW4+9sxdajE798wV1H2+PYp/rRcl/soSHIBKMPw== dependencies: "@apollo/cache-control-types" "^1.0.3" "@apollo/server-gateway-interface" "^2.0.0" From f9774a1435fd20a0da75730f716986d1d1998da4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:02:38 -0400 Subject: [PATCH 10/22] chore(deps): bump lodash-es from 4.17.23 to 4.18.1 (#1202) Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1) --- updated-dependencies: - dependency-name: lodash-es dependency-version: 4.18.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 39217b3ab..d78bafc80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6443,9 +6443,9 @@ locate-path@^7.2.0: p-locate "^6.0.0" lodash-es@^4.17.21: - version "4.17.23" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0" - integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg== + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d" + integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A== lodash.camelcase@^4.3.0: version "4.3.0" From a242e842acaf9f5526491c7a294b8a677979b48f Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 2 Apr 2026 20:01:01 -0400 Subject: [PATCH 11/22] fix: keep drawer scroll position --- src/features/drawer/index.jsx | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/features/drawer/index.jsx b/src/features/drawer/index.jsx index bc93323a3..b936f23d1 100644 --- a/src/features/drawer/index.jsx +++ b/src/features/drawer/index.jsx @@ -16,10 +16,8 @@ import { DividerWithMargin } from '@components/StyledDivider' import { DrawerActions } from './components/Actions' import { DrawerSectionMemo } from './components/Section' -const handleClose = () => useLayoutStore.setState({ drawer: false }) - const DrawerHeader = React.memo( - () => { + ({ onClose }) => { const title = useMemory((s) => s.config.general.title) return ( @@ -35,13 +33,13 @@ const DrawerHeader = React.memo( > {title} - + ) }, - () => true, + (prev, next) => prev.onClose === next.onClose, ) const listItemSx = /** @type {import('@mui/material').SxProps} */ ({ @@ -51,6 +49,21 @@ const listItemSx = /** @type {import('@mui/material').SxProps} */ ({ export function Drawer() { const drawer = useLayoutStore((s) => s.drawer) const { config, ui } = useMemory.getState() + const paperRef = React.useRef(/** @type {HTMLDivElement | null} */ (null)) + const scrollTopRef = React.useRef(0) + + const handleScroll = React.useCallback((e) => { + scrollTopRef.current = e.currentTarget.scrollTop + }, []) + + const handleClose = React.useCallback(() => { + scrollTopRef.current = paperRef.current?.scrollTop || 0 + useLayoutStore.setState({ drawer: false }) + }, []) + + const handleEnter = React.useCallback((node) => { + node.scrollTop = scrollTopRef.current + }, []) return ( - + {Object.entries(ui).map(([category, value]) => ( From abc871df6afc733cd218e2e9c84aa4fb4f9071cf Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 2 Apr 2026 20:22:32 -0400 Subject: [PATCH 12/22] chore: reorder pokestop features based on use of frequency --- server/src/ui/drawer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/ui/drawer.js b/server/src/ui/drawer.js index 3af7527df..a9491f44c 100644 --- a/server/src/ui/drawer.js +++ b/server/src/ui/drawer.js @@ -52,14 +52,14 @@ function drawer(req, perms) { } : BLOCKED, pokestops: - (perms.pokestops || perms.lures || perms.quests || perms.invasions) && + (perms.pokestops || perms.quests || perms.invasions || perms.lures) && state.db.models.Pokestop ? { allPokestops: perms.pokestops || BLOCKED, - lures: perms.lures || BLOCKED, - eventStops: perms.eventStops || BLOCKED, quests: perms.quests || BLOCKED, invasions: perms.invasions || BLOCKED, + eventStops: perms.eventStops || BLOCKED, + lures: perms.lures || BLOCKED, arEligible: perms.pokestops || BLOCKED, } : BLOCKED, From 3670810193e1c74bcd8352825083c069b33b1bff Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 2 Apr 2026 20:38:46 -0400 Subject: [PATCH 13/22] fix: properly set default discord auth as none --- config/local.example.json | 3 +- config/multi-domain-example/local.json | 3 +- packages/config/lib/mutations.js | 3 ++ packages/types/lib/augmentations.d.ts | 1 + server/src/routes/authRouter.js | 69 ++++++++++++++++++++------ 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/config/local.example.json b/config/local.example.json index 5f93a1674..45ca6d0eb 100644 --- a/config/local.example.json +++ b/config/local.example.json @@ -84,7 +84,8 @@ "redirectUri": "http://localhost:8080/auth/discord/callback", "allowedGuilds": [], "blockedGuilds": [], - "allowedUsers": [] + "allowedUsers": [], + "clientPrompt": "none" } ], "areaRestrictions": [ diff --git a/config/multi-domain-example/local.json b/config/multi-domain-example/local.json index ff737d99b..c2310c10e 100644 --- a/config/multi-domain-example/local.json +++ b/config/multi-domain-example/local.json @@ -82,7 +82,8 @@ "redirectUri": "http://localhost:8080/auth/discord/callback", "allowedGuilds": [], "blockedGuilds": [], - "allowedUsers": [] + "allowedUsers": [], + "clientPrompt": "none" } ], "alwaysEnabledPerms": ["map"], diff --git a/packages/config/lib/mutations.js b/packages/config/lib/mutations.js index aad7c2c08..58b0e7835 100644 --- a/packages/config/lib/mutations.js +++ b/packages/config/lib/mutations.js @@ -315,6 +315,9 @@ const applyMutations = (config) => { config.authentication.strategies = config.authentication.strategies.map( (strategy) => ({ ...strategy, + ...(strategy.type === 'discord' + ? { clientPrompt: strategy.clientPrompt ?? 'none' } + : {}), allowedGuilds: Array.isArray(strategy.allowedGuilds) ? strategy.allowedGuilds.flatMap(replaceAliases) : [], diff --git a/packages/types/lib/augmentations.d.ts b/packages/types/lib/augmentations.d.ts index 43f40a704..0d6ac029c 100644 --- a/packages/types/lib/augmentations.d.ts +++ b/packages/types/lib/augmentations.d.ts @@ -32,6 +32,7 @@ declare global { declare module 'express-session' { interface SessionData { tutorial: boolean + discordPromptRetry?: string } } diff --git a/server/src/routes/authRouter.js b/server/src/routes/authRouter.js index 34cd61a0c..71b0a391e 100644 --- a/server/src/routes/authRouter.js +++ b/server/src/routes/authRouter.js @@ -18,22 +18,61 @@ const loadAuthStrategies = () => { : 'post' if (strategy.enabled) { const name = strategy.name ?? `${strategy.type}-${i}` - const callbackOptions = {} - const authenticateOptions = { - failureRedirect: '/', - successRedirect: '/', - } - if (strategy.type === 'discord') { - callbackOptions.prompt = strategy.clientPrompt + const isDiscordPromptRetry = (req) => + strategy.type === 'discord' && req.session.discordPromptRetry === name + const getAuthenticateOptions = (req, includeRedirects = false) => { + const options = includeRedirects + ? { + failureRedirect: '/', + successRedirect: '/', + } + : {} + + if ( + strategy.type === 'discord' && + strategy.clientPrompt && + !isDiscordPromptRetry(req) + ) { + options.prompt = strategy.clientPrompt + } + + return options } - authRouter[method]( - `/${name}`, - passport.authenticate(name, authenticateOptions), + + authRouter[method](`/${name}`, (req, res, next) => + passport.authenticate(name, getAuthenticateOptions(req, true))( + req, + res, + next, + ), ) - authRouter[method](`/${name}/callback`, async (req, res, next) => - passport.authenticate( + authRouter[method](`/${name}/callback`, async (req, res, next) => { + if ( + strategy.type === 'discord' && + strategy.clientPrompt === 'none' && + !isDiscordPromptRetry(req) && + typeof req.query.error === 'string' + ) { + req.session.discordPromptRetry = name + log.debug( + TAGS.auth, + 'Discord silent auth needs user interaction, retrying with approval page', + ) + return res.redirect(`${req.baseUrl}/${name}/callback`) + } + + if ( + strategy.type === 'discord' && + isDiscordPromptRetry(req) && + (typeof req.query.code === 'string' || + typeof req.query.error === 'string') + ) { + delete req.session.discordPromptRetry + } + + return passport.authenticate( name, - callbackOptions, + getAuthenticateOptions(req), async (err, user, info) => { if (err) { return next(err) @@ -67,8 +106,8 @@ const loadAuthStrategies = () => { } } }, - )(req, res, next), - ) + )(req, res, next) + }) log.info( TAGS.auth, `${method.toUpperCase()} /auth/${name}/callback route initialized`, From d6769c727f57c1fc381faaabebc95e19d4331d40 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 3 Apr 2026 03:50:22 +0000 Subject: [PATCH 14/22] chore(release): v1.41.2-develop.2 [skip ci] ## [1.41.2-develop.2](https://github.com/WatWowMap/ReactMap/compare/v1.41.2-develop.1...v1.41.2-develop.2) (2026-04-03) ### Bug Fixes * keep drawer scroll position ([a242e84](https://github.com/WatWowMap/ReactMap/commit/a242e842acaf9f5526491c7a294b8a677979b48f)) * properly set default discord auth as none ([3670810](https://github.com/WatWowMap/ReactMap/commit/3670810193e1c74bcd8352825083c069b33b1bff)) --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0178751..d30631561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [1.41.2-develop.2](https://github.com/WatWowMap/ReactMap/compare/v1.41.2-develop.1...v1.41.2-develop.2) (2026-04-03) + + +### Bug Fixes + +* keep drawer scroll position ([a242e84](https://github.com/WatWowMap/ReactMap/commit/a242e842acaf9f5526491c7a294b8a677979b48f)) +* properly set default discord auth as none ([3670810](https://github.com/WatWowMap/ReactMap/commit/3670810193e1c74bcd8352825083c069b33b1bff)) + ## [1.41.2-develop.1](https://github.com/WatWowMap/ReactMap/compare/v1.41.1...v1.41.2-develop.1) (2026-03-26) diff --git a/package.json b/package.json index 7c2209673..f0d53f83b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reactmap", - "version": "1.41.2-develop.1", + "version": "1.41.2-develop.2", "private": true, "description": "React based frontend map.", "license": "MIT", From 184ac2fa3fbc372d896f70bf36277fa1dd2e6a22 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 3 Apr 2026 00:37:21 -0400 Subject: [PATCH 15/22] fix: weather update consistency --- src/features/weather/ActiveWeather.jsx | 46 +++++++++----------------- src/features/weather/WeatherPopup.jsx | 7 ++-- src/pages/map/components/Container.jsx | 2 -- src/pages/map/components/QueryData.jsx | 14 +++++--- 4 files changed, 28 insertions(+), 41 deletions(-) diff --git a/src/features/weather/ActiveWeather.jsx b/src/features/weather/ActiveWeather.jsx index d7ffcf2d3..e43c45f25 100644 --- a/src/features/weather/ActiveWeather.jsx +++ b/src/features/weather/ActiveWeather.jsx @@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next' import { useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' -import { apolloClient } from '@services/apollo' import { Header } from '@components/dialogs/Header' import { Footer } from '@components/dialogs/Footer' import { Img } from '@components/Img' @@ -89,42 +88,27 @@ function Weather({ gameplay_condition, ...props }) { ) } -const WeatherMemo = React.memo( - Weather, - (prev, next) => prev.gameplay_condition === next.gameplay_condition, -) - -export function ActiveWeather() { +/** + * @param {{ weatherData: import('@rm/types').Weather[] }} props + */ +export function ActiveWeather({ weatherData }) { const weatherEnabled = useStorage((s) => s.filters?.weather?.enabled ?? false) const location = useStorage((s) => s.location) const zoom = useStorage((s) => s.zoom) const allowedZoom = useMemory((s) => s.config.general.activeWeatherZoom) - const [active, setActive] = React.useState( - /** @type {import('@rm/types').Weather | null} */ (null), + const active = React.useMemo( + () => + zoom > allowedZoom + ? weatherData.find( + (cell) => + Array.isArray(cell?.polygon) && + booleanPointInPolygon(point(location), polygon([cell.polygon])), + ) || null + : null, + [allowedZoom, location, weatherData, zoom], ) - React.useEffect(() => { - if (zoom > allowedZoom) { - const weatherCache = Object.values(apolloClient.cache.extract()).find( - (x) => - x.__typename === 'Weather' && - // @ts-ignore - booleanPointInPolygon(point(location), polygon([x.polygon])), - ) - if ( - weatherCache && - 'gameplay_condition' in weatherCache && - weatherCache?.gameplay_condition !== active?.gameplay_condition - ) { - // @ts-ignore - setActive(weatherCache) - } - } else { - setActive(null) - } - }, [location, zoom, allowedZoom]) - if (!weatherEnabled || !active) return null - return + return } diff --git a/src/features/weather/WeatherPopup.jsx b/src/features/weather/WeatherPopup.jsx index 541aff818..1fad5dff6 100644 --- a/src/features/weather/WeatherPopup.jsx +++ b/src/features/weather/WeatherPopup.jsx @@ -89,11 +89,12 @@ const Timer = ({ updated, ts = Date.now() / 1000 }) => { : 'error.main' React.useEffect(() => { - const timer2 = setTimeout(() => { + setTimer(getTimeUntil(updated * 1000)) + const timerId = setInterval(() => { setTimer(getTimeUntil(updated * 1000)) }, 1000) - return () => clearTimeout(timer2) - }) + return () => clearInterval(timerId) + }, [updated]) return ( <> diff --git a/src/pages/map/components/Container.jsx b/src/pages/map/components/Container.jsx index ffacef9a8..15db7a1a9 100644 --- a/src/pages/map/components/Container.jsx +++ b/src/pages/map/components/Container.jsx @@ -7,7 +7,6 @@ import { useStorage } from '@store/useStorage' import { useMapStore } from '@store/useMapStore' import { ScanOnDemand } from '@features/scanner' import { WebhookMarker, WebhookAreaSelection } from '@features/webhooks' -import { ActiveWeather } from '@features/weather' import { timeCheck } from '@utils/timeCheck' import { Effects } from './Effects' @@ -66,7 +65,6 @@ export function Container() {