Skip to content

Commit a32b97d

Browse files
committed
fix: distinguish normal/unset form when they both spawn
1 parent 8292b83 commit a32b97d

10 files changed

Lines changed: 183 additions & 66 deletions

File tree

src/components/Menu.jsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useLayoutStore } from '@store/useLayoutStore'
1414
import { useFilter } from '@hooks/useFilter'
1515
import { Footer } from '@components/dialogs/Footer'
1616
import { applyToAll } from '@utils/applyToAll'
17+
import { getAmbiguousForms } from '@utils/getAmbiguousForms'
1718
import { useGetAvailable } from '@hooks/useGetAvailable'
1819
import { applyToAllWebhooks, useWebhookStore } from '@store/useWebhookStore'
1920
import { useAnalytics } from '@hooks/useAnalytics'
@@ -156,11 +157,26 @@ export function Menu({
156157
}
157158

158159
function Results({ category, webhookCategory, categories, children }) {
159-
const { t } = useTranslation()
160+
const { t, i18n } = useTranslation()
160161
const filteredArr = useFilter(category, webhookCategory, categories)
162+
const available = useMemory((s) => s.available[category])
163+
164+
// available keeps the labels stable while the anomaly lasts, filteredArr
165+
// covers the menu options that render more than what's currently available
166+
const ambiguousForms = React.useMemo(
167+
() => getAmbiguousForms([...(available || []), ...filteredArr]),
168+
[available, filteredArr, i18n.language],
169+
)
170+
171+
// not via VirtualGrid's context: that one gets spread onto the Grid2 items
172+
const itemContent = React.useCallback(
173+
(index, key) => children(index, key, ambiguousForms),
174+
[children, ambiguousForms],
175+
)
176+
161177
return filteredArr.length ? (
162178
<VirtualGrid data={filteredArr} xs={4} md={2}>
163-
{children}
179+
{itemContent}
164180
</VirtualGrid>
165181
) : (
166182
<Box className="flex-center" flex="1 1 auto" whiteSpace="pre-line">

src/components/filters/Advanced.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { GenderListItem } from './Gender'
2626
import { QuestConditionSelector } from './QuestConditions'
2727

2828
export function AdvancedFilter() {
29-
const { category, id, selectedIds, open } = useLayoutStore(
29+
const { category, id, selectedIds, open, showDefaultForms } = useLayoutStore(
3030
(s) => s.advancedFilter,
3131
)
3232
const { t } = useTranslation()
@@ -134,7 +134,7 @@ export function AdvancedFilter() {
134134
: t('set_size')
135135
} - ${tId(id, {
136136
omitFormSuffix: true,
137-
showDefaultForms: id.startsWith('a'),
137+
showDefaultForms: id.startsWith('a') || showDefaultForms,
138138
})}`}
139139
action={() => toggleClose(false)}
140140
/>

src/components/filters/FilterMenu.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ export function FilterMenu() {
3838
names={[category]}
3939
/>
4040
<Menu category={category} extraButtons={EXTRA_BUTTONS}>
41-
{(_, key) => <StandardItem id={key} category={category} caption />}
41+
{(_, key, ambiguousForms) => (
42+
<StandardItem
43+
id={key}
44+
category={category}
45+
ambiguousForms={ambiguousForms}
46+
caption
47+
/>
48+
)}
4249
</Menu>
4350
</DialogWrapper>
4451
)

src/components/virtual/SelectorItem.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Tooltip from '@mui/material/Tooltip'
66
import Box from '@mui/material/Box'
77

88
import { useTranslateById } from '@hooks/useTranslateById'
9+
import { hasAmbiguousForm } from '@utils/getAmbiguousForms'
910
import { useMemory } from '@store/useMemory'
1011
import { ColoredTile } from '@components/virtual/ColoredTile'
1112
import { ToggleTypography } from '@components/ToggleTypography'
@@ -19,6 +20,7 @@ import { useTranslation } from 'react-i18next'
1920
* id: string,
2021
* category: T,
2122
* caption?: boolean
23+
* ambiguousForms?: Set<string>
2224
* }} BaseProps
2325
*/
2426

@@ -31,6 +33,7 @@ import { useTranslation } from 'react-i18next'
3133
* hasAll?: boolean
3234
* easyMode?: boolean
3335
* caption?: boolean
36+
* ambiguousForms?: Set<string>
3437
* }} FullProps
3538
*/
3639

@@ -43,13 +46,15 @@ export function SelectorItem({
4346
onClick,
4447
hasAll,
4548
easyMode,
49+
ambiguousForms,
4650
}) {
4751
const { t } = useTranslateById({
4852
alt: true,
4953
newLine: true,
5054
quest: category === 'pokestops',
5155
omitFormSuffix: true,
52-
showDefaultForms: id.startsWith('a'),
56+
showDefaultForms:
57+
id.startsWith('a') || hasAmbiguousForm(ambiguousForms, id),
5358
})
5459
const title = t(id)
5560
const url = useMemory((s) => s.Icons.getIconById(id))

src/components/virtual/StandardItem.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import * as React from 'react'
44
import { useLayoutStore } from '@store/useLayoutStore'
55
import { useDeepStore, useStorage } from '@store/useStorage'
66
import { checkIfHasAll } from '@utils/hasAll'
7+
import { hasAmbiguousForm } from '@utils/getAmbiguousForms'
78

89
import { SelectorItem } from './SelectorItem'
910

1011
/** @param {import('./SelectorItem').BaseProps<keyof import('@rm/types').Available>} props */
11-
export function StandardItem({ id, category, ...props }) {
12+
export function StandardItem({ id, category, ambiguousForms, ...props }) {
1213
const [filter, setFilter] = useDeepStore(`filters.${category}.filter.${id}`)
1314
const hasAll = checkIfHasAll(category, id)
1415
const easyMode = useStorage((s) => !!s.filters?.[category]?.easyMode)
@@ -21,6 +22,7 @@ export function StandardItem({ id, category, ...props }) {
2122
setFilter={setFilter}
2223
hasAll={hasAll}
2324
easyMode={easyMode}
25+
ambiguousForms={ambiguousForms}
2426
onClick={() =>
2527
useLayoutStore.setState(
2628
id.startsWith('t')
@@ -31,6 +33,7 @@ export function StandardItem({ id, category, ...props }) {
3133
id,
3234
category,
3335
selectedIds: [],
36+
showDefaultForms: hasAmbiguousForm(ambiguousForms, id),
3437
},
3538
},
3639
)

src/features/drawer/components/SelectorList.jsx

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useMemory } from '@store/useMemory'
2121
import { useLayoutStore } from '@store/useLayoutStore'
2222
import { useDeepStore, useStorage } from '@store/useStorage'
2323
import { useGetAvailable } from '@hooks/useGetAvailable'
24+
import { getAmbiguousForms, hasAmbiguousForm } from '@utils/getAmbiguousForms'
2425
import { VirtualGrid } from '@components/virtual/VirtualGrid'
2526
import { TabPanel } from '@components/TabPanel'
2627
import { BoolToggle } from '@components/inputs/BoolToggle'
@@ -81,53 +82,66 @@ function SelectorList({
8182
const search = useStorage((s) => s.searches[searchKey] || '')
8283
const disableGutters = !['pokemon', 'tappables'].includes(category)
8384

84-
const translated = React.useMemo(
85-
() =>
86-
(onlyShowAvailable ? available : Object.keys(allFilters))
87-
.filter((key) => {
88-
if (key === 'global') return false
89-
switch (subCategory) {
90-
case 'raids':
91-
return key.startsWith('e')
92-
case 'lures':
93-
return key.startsWith('l')
94-
case 'invasions':
95-
return key.startsWith('i')
96-
case 'quests':
97-
return (
98-
key.startsWith('q') ||
99-
key.startsWith('m') ||
100-
key.startsWith('x') ||
101-
key.startsWith('c') ||
102-
key.startsWith('d') ||
103-
key.startsWith('p') ||
104-
key.startsWith('u')
105-
)
106-
case 'showcase':
107-
return (
108-
key.startsWith('f') ||
109-
key.startsWith('h') ||
110-
key.startsWith('y') ||
111-
key.startsWith('b')
112-
)
113-
case 'rocketPokemon':
114-
return key.startsWith('a')
115-
case 'pokemon':
116-
return Number.isInteger(Number(key.charAt(0)))
85+
const { translated, ambiguousForms } = React.useMemo(() => {
86+
const ids = (
87+
onlyShowAvailable ? available : Object.keys(allFilters)
88+
).filter((key) => {
89+
if (key === 'global') return false
90+
switch (subCategory) {
91+
case 'raids':
92+
return key.startsWith('e')
93+
case 'lures':
94+
return key.startsWith('l')
95+
case 'invasions':
96+
return key.startsWith('i')
97+
case 'quests':
98+
return (
99+
key.startsWith('q') ||
100+
key.startsWith('m') ||
101+
key.startsWith('x') ||
102+
key.startsWith('c') ||
103+
key.startsWith('d') ||
104+
key.startsWith('p') ||
105+
key.startsWith('u')
106+
)
107+
case 'showcase':
108+
return (
109+
key.startsWith('f') ||
110+
key.startsWith('h') ||
111+
key.startsWith('y') ||
112+
key.startsWith('b')
113+
)
114+
case 'rocketPokemon':
115+
return key.startsWith('a')
116+
case 'pokemon':
117+
return Number.isInteger(Number(key.charAt(0)))
118+
default:
119+
switch (category) {
120+
case 'gyms':
121+
return key.startsWith('t')
122+
case 'tappables':
123+
return key.startsWith('q') && key !== 'q0'
117124
default:
118-
switch (category) {
119-
case 'gyms':
120-
return key.startsWith('t')
121-
case 'tappables':
122-
return key.startsWith('q') && key !== 'q0'
123-
default:
124-
return Number.isInteger(Number(key.charAt(0)))
125-
}
125+
return Number.isInteger(Number(key.charAt(0)))
126126
}
127-
})
128-
.map((id) => ({ id, name: tId(id).toLowerCase() })),
129-
[onlyShowAvailable ? available : allFilters, tId, category, subCategory],
130-
)
127+
}
128+
})
129+
// two identical looking entries (e.g. Litleo Unset + Normal) must show
130+
// their form to be told apart, but only for as long as both are around
131+
const ambiguous = getAmbiguousForms(ids)
132+
return {
133+
ambiguousForms: ambiguous,
134+
translated: ids.map((id) => ({
135+
id,
136+
name: tId(id, {
137+
quest: subCategory === 'pokemon',
138+
omitFormSuffix: true,
139+
showDefaultForms:
140+
subCategory === 'rocketPokemon' || hasAmbiguousForm(ambiguous, id),
141+
}).toLowerCase(),
142+
})),
143+
}
144+
}, [onlyShowAvailable ? available : allFilters, tId, category, subCategory])
131145

132146
const items = React.useMemo(() => {
133147
const lowerCase = search.toLowerCase()
@@ -249,7 +263,13 @@ function SelectorList({
249263
restoreStateFrom={restoreStateFrom}
250264
stateChanged={handleStateChanged}
251265
>
252-
{(_, key) => <StandardItem id={key} category={category} />}
266+
{(_, key) => (
267+
<StandardItem
268+
id={key}
269+
category={category}
270+
ambiguousForms={ambiguousForms}
271+
/>
272+
)}
253273
</VirtualGrid>
254274
</Box>
255275
</List>

src/features/webhooks/Manage.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,14 @@ export function Manage() {
149149
webhookCategory={category}
150150
extraButtons={buttons}
151151
>
152-
{(_, key) => <WebhookItem id={key} category={category} caption />}
152+
{(_, key, ambiguousForms) => (
153+
<WebhookItem
154+
id={key}
155+
category={category}
156+
ambiguousForms={ambiguousForms}
157+
caption
158+
/>
159+
)}
153160
</Menu>
154161
</>
155162
) : (

src/hooks/useGetAvailable.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,22 @@ export function useGetAvailable(category) {
2424
)
2525

2626
useEffect(() => {
27-
if (data?.[`available${capitalized}`]) {
28-
useMemory.setState((prev) => ({
29-
available: {
30-
...prev.available,
31-
[category]: data[`available${capitalized}`].some(
32-
(key, i) => key !== prev.available[category][i],
33-
)
34-
? data[`available${capitalized}`]
35-
: prev.available[category],
36-
// if it's the same, don't cause re-renders
37-
},
38-
}))
27+
const next = data?.[`available${capitalized}`]
28+
if (next) {
29+
useMemory.setState((prev) => {
30+
const previous = prev.available[category] || []
31+
return {
32+
available: {
33+
...prev.available,
34+
// if it's the same, don't cause re-renders
35+
[category]:
36+
next.length !== previous.length ||
37+
next.some((key, i) => key !== previous[i])
38+
? next
39+
: previous,
40+
},
41+
}
42+
})
3943
}
4044
}, [data])
4145

src/store/useLayoutStore.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { useStorage } from './useStorage'
2828
* category: import('@rm/types').AdvCategories | '',
2929
* id: string,
3030
* selectedIds: string[],
31+
* showDefaultForms?: boolean,
3132
* },
3233
* dialog: {
3334
* open: boolean,
@@ -64,6 +65,7 @@ export const useLayoutStore = create(() => ({
6465
category: '',
6566
id: '',
6667
selectedIds: [],
68+
showDefaultForms: false,
6769
},
6870
dialog: {
6971
open: false,

src/utils/getAmbiguousForms.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// @ts-check
2+
import { getFormDisplay } from './getFormDisplay'
3+
4+
const WILD_KEY = /^(\d+)-(\d+)$/
5+
6+
/**
7+
* Finds the Pokemon that would render multiple visually identical entries in a
8+
* list, since default form labels are hidden: e.g. when the scanner reports
9+
* both `667-0` (Unset) and `667-3036` (Normal), both collapse to "Litleo".
10+
*
11+
* Two of them spawning at once is an anomaly, so this is evaluated against the
12+
* exact list being rendered - once only one is left, the labels collapse again.
13+
*
14+
* @param {string[]} ids the filter keys that are about to be rendered
15+
* @returns {Set<string>} the Pokemon ids whose form labels must stay visible
16+
*/
17+
export function getAmbiguousForms(ids) {
18+
/** @type {Map<string, Set<string>>} */
19+
const labels = new Map()
20+
/** @type {Set<string>} */
21+
const ambiguous = new Set()
22+
// availability lists are not necessarily unique: Nest.getAvailable() maps
23+
// both a form 0 row and an explicit default form row to the same key, and a
24+
// key repeated for one form is not two entries to tell apart
25+
new Set(ids).forEach((id) => {
26+
const match = WILD_KEY.exec(id)
27+
if (!match) return
28+
const [, pokemonId, form] = match
29+
const label = getFormDisplay(pokemonId, form, undefined, {
30+
appendFormSuffix: false,
31+
})
32+
const seen = labels.get(pokemonId)
33+
if (!seen) {
34+
labels.set(pokemonId, new Set([label]))
35+
} else if (seen.has(label)) {
36+
ambiguous.add(pokemonId)
37+
} else {
38+
seen.add(label)
39+
}
40+
})
41+
return ambiguous
42+
}
43+
44+
/**
45+
* @param {Set<string> | undefined} ambiguous result of {@link getAmbiguousForms}
46+
* @param {string} id
47+
* @returns {boolean}
48+
*/
49+
export function hasAmbiguousForm(ambiguous, id) {
50+
if (!ambiguous?.size) return false
51+
const match = WILD_KEY.exec(id)
52+
return !!match && ambiguous.has(match[1])
53+
}

0 commit comments

Comments
 (0)