From 3bf73fe4750f59e1b5615614894e9645334c6ba6 Mon Sep 17 00:00:00 2001 From: lenisko <10072920+lenisko@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:48:12 +0100 Subject: [PATCH 1/3] feat: hidden persistence --- packages/locales/lib/human/en.json | 2 + packages/locales/lib/human/pl.json | 2 + src/features/drawer/settings/index.jsx | 11 ++ src/features/gym/GymPopup.jsx | 4 +- src/features/nest/NestPopup.jsx | 4 +- src/features/pokemon/PokemonPopup.jsx | 3 +- src/features/pokestop/PokestopPopup.jsx | 4 +- src/features/station/StationPopup.jsx | 9 +- src/features/tappable/TappablePopup.jsx | 9 +- src/store/useMemory.js | 4 +- src/utils/pokemon/hiddenPokemon.js | 155 ++++++++++++++++++++++++ 11 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 src/utils/pokemon/hiddenPokemon.js diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index b99cb961e..966dcfd7f 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -138,6 +138,8 @@ "exclude_lure": "Exclude Lure", "timer": "Timer", "hide": "Hide", + "hidden_for_hour": "Hidden for an hour", + "clean_hidden": "Clean Hidden", "tier": "Tier", "slots": "Slots", "mega": "Mega", diff --git a/packages/locales/lib/human/pl.json b/packages/locales/lib/human/pl.json index 077c4b17b..cf3310edf 100644 --- a/packages/locales/lib/human/pl.json +++ b/packages/locales/lib/human/pl.json @@ -228,6 +228,8 @@ "has_quest_indicator": "Alternatywny kolor dla Pokéstopów z zadaniami", "help": "Pomoc", "hide": "Ukryj", + "hidden_for_hour": "Schowano na godzinę", + "clean_hidden": "Wyczyść schowane", "hide_editor": "Ukryj edytor", "historic_rarity": "Rzadkość historyczna", "hisuian": "Hisuian", diff --git a/src/features/drawer/settings/index.jsx b/src/features/drawer/settings/index.jsx index 69254ecb8..875e5691e 100644 --- a/src/features/drawer/settings/index.jsx +++ b/src/features/drawer/settings/index.jsx @@ -9,6 +9,7 @@ import InsightsIcon from '@mui/icons-material/Insights' import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive' import NotificationsOffIcon from '@mui/icons-material/NotificationsOff' import LogoDevIcon from '@mui/icons-material/LogoDev' +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff' import { useTranslation } from 'react-i18next' import { useMemory } from '@store/useMemory' @@ -22,6 +23,7 @@ import { LocaleSelection } from '@components/inputs/LocaleSelection' import { DividerWithMargin } from '@components/StyledDivider' import { BoolToggle } from '@components/inputs/BoolToggle' import { BasicListButton } from '@components/inputs/BasicListButton' +import { clearHiddenEntities } from '@utils/pokemon/hiddenPokemon' import { DrawerActions } from '../components/Actions' import { GeneralSetting } from './General' @@ -70,6 +72,15 @@ export function Settings() { )} + { + clearHiddenEntities() + useMemory.setState({ hideList: new Set() }) + }} + label="clean_hidden" + > + + diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index a05b62b33..e64b143d2 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -37,6 +37,7 @@ import { getTimeUntil } from '@utils/getTimeUntil' import { formatInterval } from '@utils/formatInterval' import { usePokemonBackgroundVisuals } from '@hooks/usePokemonBackgroundVisuals' import { getFormDisplay } from '@utils/getFormDisplay' +import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' import { useWebhook } from './useWebhook' @@ -747,7 +748,8 @@ const DropdownOptions = ({ const handleHide = () => { handleClose() - useMemory.setState((prev) => ({ hideList: new Set(prev.hideList).add(id) })) + useMemory.setState({ hideList: addHiddenEntity(id) }) + showHideSnackbar(t('hidden_for_hour')) } const handleExclude = (key) => { diff --git a/src/features/nest/NestPopup.jsx b/src/features/nest/NestPopup.jsx index 35c55c377..7324e2e7d 100644 --- a/src/features/nest/NestPopup.jsx +++ b/src/features/nest/NestPopup.jsx @@ -16,6 +16,7 @@ import { setDeepStore } from '@store/useStorage' import { getTimeUntil } from '@utils/getTimeUntil' import { useAnalytics } from '@hooks/useAnalytics' import { Navigation } from '@components/popups/Navigation' +import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' /** @param {number} timeSince */ const getColor = (timeSince) => { @@ -62,7 +63,8 @@ export function NestPopup({ const handleClose = () => setAnchorEl(null) const handleHide = () => { setAnchorEl(null) - useMemory.setState((prev) => ({ hideList: new Set(prev.hideList).add(id) })) + useMemory.setState({ hideList: addHiddenEntity(id) }) + showHideSnackbar(t('hidden_for_hour')) } const handleExclude = () => { diff --git a/src/features/pokemon/PokemonPopup.jsx b/src/features/pokemon/PokemonPopup.jsx index f88937713..87903e950 100644 --- a/src/features/pokemon/PokemonPopup.jsx +++ b/src/features/pokemon/PokemonPopup.jsx @@ -33,6 +33,7 @@ import { GET_TAPPABLE_BY_ID } from '@services/queries/tappable' import { usePokemonBackgroundVisual } from '@hooks/usePokemonBackgroundVisuals' import { BackgroundCard } from '@components/popups/BackgroundCard' import { getFormDisplay } from '@utils/getFormDisplay' +import { addHiddenEntity } from '@utils/pokemon/hiddenPokemon' const rowClass = { width: 30, fontWeight: 'bold' } @@ -357,7 +358,7 @@ const Header = ({ pokemon, metaData, iconUrl, userSettings, isTutorial }) => { const handleHide = () => { setAnchorEl(null) - useMemory.setState((prev) => ({ hideList: new Set(prev.hideList).add(id) })) + useMemory.setState({ hideList: addHiddenEntity(id) }) } const handleExclude = () => { diff --git a/src/features/pokestop/PokestopPopup.jsx b/src/features/pokestop/PokestopPopup.jsx index 14a9fbde9..7523f006b 100644 --- a/src/features/pokestop/PokestopPopup.jsx +++ b/src/features/pokestop/PokestopPopup.jsx @@ -35,6 +35,7 @@ import { useGetAvailable } from '@hooks/useGetAvailable' import { parseQuestConditions } from '@utils/parseConditions' import { Img } from '@components/Img' import { readableProbability } from '@utils/readableProbability' +import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' import { usePokemonBackgroundVisuals, usePokemonBackgroundVisual, @@ -341,7 +342,8 @@ const MenuActions = ({ const handleHide = () => { setAnchorEl(null) - useMemory.setState((prev) => ({ hideList: new Set(prev.hideList).add(id) })) + useMemory.setState({ hideList: addHiddenEntity(id) }) + showHideSnackbar(t('hidden_for_hour')) } /** @param {string} key */ diff --git a/src/features/station/StationPopup.jsx b/src/features/station/StationPopup.jsx index 5b13d60ed..d32040747 100644 --- a/src/features/station/StationPopup.jsx +++ b/src/features/station/StationPopup.jsx @@ -27,6 +27,7 @@ import { Img, PokemonImg } from '@components/Img' import { useFormatStore } from '@store/useFormatStore' import { useRelativeTimer } from '@hooks/useRelativeTime' import { useAnalytics } from '@hooks/useAnalytics' +import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' import { BackgroundCard } from '@components/popups/BackgroundCard' import { Title } from '@components/popups/Title' import { @@ -169,10 +170,10 @@ function StationMenu({ () => [ { name: 'hide', - action: () => - useMemory.setState((prev) => ({ - hideList: new Set(prev.hideList).add(id), - })), + action: () => { + useMemory.setState({ hideList: addHiddenEntity(id) }) + showHideSnackbar(t('hidden_for_hour')) + }, }, { name: 'exclude_battle', diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index 0da263d43..d8d9bdc9e 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -20,6 +20,8 @@ import { StatusIcon } from '@components/StatusIcon' import { Title } from '@components/popups/Title' import { getTimeUntil } from '@utils/getTimeUntil' +import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' + import { getTappableDisplaySettings } from './displayRules' /** @@ -107,10 +109,9 @@ export function TappablePopup({ tappable, rewardIcon }) { const handleHide = React.useCallback(() => { setMenuAnchorEl(null) if (tappable.id === undefined || tappable.id === null) return - useMemory.setState((prev) => ({ - hideList: new Set(prev.hideList).add(tappable.id), - })) - }, [tappable.id]) + useMemory.setState({ hideList: addHiddenEntity(tappable.id) }) + showHideSnackbar(t('hidden_for_hour')) + }, [tappable.id, t]) const handleExclude = React.useCallback(() => { setMenuAnchorEl(null) diff --git a/src/store/useMemory.js b/src/store/useMemory.js index d4ed1630c..18ff6c990 100644 --- a/src/store/useMemory.js +++ b/src/store/useMemory.js @@ -2,6 +2,8 @@ import { create } from 'zustand' +import { getHiddenEntitySet } from '@utils/pokemon/hiddenPokemon' + /** * TODO: Finish this * @typedef {{ @@ -151,7 +153,7 @@ export const useMemory = create(() => ({ locationCards: {}, routeTypes: {}, }, - hideList: new Set(), + hideList: getHiddenEntitySet(), timerList: [], timeOfDay: 'day', extraUserFields: [], diff --git a/src/utils/pokemon/hiddenPokemon.js b/src/utils/pokemon/hiddenPokemon.js new file mode 100644 index 000000000..0a87b22e5 --- /dev/null +++ b/src/utils/pokemon/hiddenPokemon.js @@ -0,0 +1,155 @@ +// @ts-check + +const STORAGE_KEY = 'pokemon-hide-list' +const SNACKBAR_COUNT_KEY = 'pokemon-hide-snackbar-count' +const MAX_AGE_MS = 60 * 60 * 1000 // 1 hour +const MAX_SNACKBAR_SHOWS = 3 + +/** + * @typedef {{ id: string | number, ts: number }} HiddenEntry + */ + +/** + * Load hidden Pokemon entries from localStorage + * @returns {HiddenEntry[]} + */ +function loadEntries() { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + const parsed = JSON.parse(raw) + return Array.isArray(parsed) ? parsed : [] + } catch { + return [] + } +} + +/** + * Save hidden Pokemon entries to localStorage + * @param {HiddenEntry[]} entries + */ +function saveEntries(entries) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(entries)) + } catch { + // localStorage may be full or disabled + } +} + +/** + * Clean entries older than 1 hour + * @param {HiddenEntry[]} entries + * @returns {HiddenEntry[]} + */ +function cleanOldEntries(entries) { + const now = Date.now() + return entries.filter((e) => now - e.ts < MAX_AGE_MS) +} + +/** + * Add an entity ID to the hidden list with timestamp, cleaning old entries + * @param {string | number} id + * @returns {Set} Updated hideList Set + */ +export function addHiddenEntity(id) { + const entries = cleanOldEntries(loadEntries()) + if (!entries.some((e) => e.id === id)) { + entries.push({ id, ts: Date.now() }) + } + saveEntries(entries) + return new Set(entries.map((e) => e.id)) +} + +/** + * Get the current hidden entity Set from localStorage (cleaned) + * @returns {Set} + */ +export function getHiddenEntitySet() { + const entries = cleanOldEntries(loadEntries()) + saveEntries(entries) // persist cleaned list + return new Set(entries.map((e) => e.id)) +} + +/** @type {{ current: number | null }} */ +const snackbarTimer = { current: null } + +/** @type {{ current: HTMLDivElement | null }} */ +const snackbarRef = { current: null } + +/** + * Get snackbar show count from localStorage + * @returns {number} + */ +function getSnackbarCount() { + try { + return parseInt(localStorage.getItem(SNACKBAR_COUNT_KEY) || '0', 10) + } catch { + return 0 + } +} + +/** + * Increment snackbar show count in localStorage + */ +function incrementSnackbarCount() { + try { + const count = getSnackbarCount() + 1 + localStorage.setItem(SNACKBAR_COUNT_KEY, String(count)) + } catch { + // localStorage may be full or disabled + } +} + +/** + * Show a temporary snackbar message for 2 seconds (max 3 times total) + * @param {string} message + */ +export function showHideSnackbar(message) { + if (getSnackbarCount() >= MAX_SNACKBAR_SHOWS) { + return + } + + if (snackbarTimer.current) { + clearTimeout(snackbarTimer.current) + } + if (snackbarRef.current) { + snackbarRef.current.remove() + } + + incrementSnackbarCount() + + const snackbar = document.createElement('div') + snackbar.textContent = message + snackbar.style.cssText = ` + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: rgba(50, 50, 50, 0.95); + color: white; + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + z-index: 10000; + pointer-events: none; + ` + document.body.appendChild(snackbar) + snackbarRef.current = snackbar + + snackbarTimer.current = window.setTimeout(() => { + snackbar.remove() + snackbarRef.current = null + snackbarTimer.current = null + }, 2000) +} + +/** + * Clear all hidden entities from localStorage + */ +export function clearHiddenEntities() { + try { + localStorage.removeItem(STORAGE_KEY) + } catch { + // localStorage may be disabled + } +} From 92fa16b90f971be5bd3ff80cc560907b9273fd59 Mon Sep 17 00:00:00 2001 From: lenisko <10072920+lenisko@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:14:18 +0100 Subject: [PATCH 2/3] fix: rename utils for cleanup fix: clean outdated storage 15s after map loads --- src/features/drawer/settings/index.jsx | 2 +- src/features/gym/GymPopup.jsx | 2 +- src/features/nest/NestPopup.jsx | 2 +- src/features/pokemon/PokemonPopup.jsx | 2 +- src/features/pokestop/PokestopPopup.jsx | 2 +- src/features/station/StationPopup.jsx | 2 +- src/features/tappable/TappablePopup.jsx | 2 +- src/pages/map/components/Container.jsx | 9 ++++++ src/store/useMemory.js | 2 +- .../hiddenPokemon.js => hiddenEntities.js} | 31 +++++++++++++------ 10 files changed, 39 insertions(+), 17 deletions(-) rename src/utils/{pokemon/hiddenPokemon.js => hiddenEntities.js} (79%) diff --git a/src/features/drawer/settings/index.jsx b/src/features/drawer/settings/index.jsx index 875e5691e..d07f2eba6 100644 --- a/src/features/drawer/settings/index.jsx +++ b/src/features/drawer/settings/index.jsx @@ -23,7 +23,7 @@ import { LocaleSelection } from '@components/inputs/LocaleSelection' import { DividerWithMargin } from '@components/StyledDivider' import { BoolToggle } from '@components/inputs/BoolToggle' import { BasicListButton } from '@components/inputs/BasicListButton' -import { clearHiddenEntities } from '@utils/pokemon/hiddenPokemon' +import { clearHiddenEntities } from '@utils/hiddenEntities' import { DrawerActions } from '../components/Actions' import { GeneralSetting } from './General' diff --git a/src/features/gym/GymPopup.jsx b/src/features/gym/GymPopup.jsx index e64b143d2..d7905ba56 100644 --- a/src/features/gym/GymPopup.jsx +++ b/src/features/gym/GymPopup.jsx @@ -37,7 +37,7 @@ import { getTimeUntil } from '@utils/getTimeUntil' import { formatInterval } from '@utils/formatInterval' import { usePokemonBackgroundVisuals } from '@hooks/usePokemonBackgroundVisuals' import { getFormDisplay } from '@utils/getFormDisplay' -import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' +import { addHiddenEntity, showHideSnackbar } from '@utils/hiddenEntities' import { useWebhook } from './useWebhook' diff --git a/src/features/nest/NestPopup.jsx b/src/features/nest/NestPopup.jsx index 7324e2e7d..9df705834 100644 --- a/src/features/nest/NestPopup.jsx +++ b/src/features/nest/NestPopup.jsx @@ -16,7 +16,7 @@ import { setDeepStore } from '@store/useStorage' import { getTimeUntil } from '@utils/getTimeUntil' import { useAnalytics } from '@hooks/useAnalytics' import { Navigation } from '@components/popups/Navigation' -import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' +import { addHiddenEntity, showHideSnackbar } from '@utils/hiddenEntities' /** @param {number} timeSince */ const getColor = (timeSince) => { diff --git a/src/features/pokemon/PokemonPopup.jsx b/src/features/pokemon/PokemonPopup.jsx index 87903e950..3fc5dbf2b 100644 --- a/src/features/pokemon/PokemonPopup.jsx +++ b/src/features/pokemon/PokemonPopup.jsx @@ -33,7 +33,7 @@ import { GET_TAPPABLE_BY_ID } from '@services/queries/tappable' import { usePokemonBackgroundVisual } from '@hooks/usePokemonBackgroundVisuals' import { BackgroundCard } from '@components/popups/BackgroundCard' import { getFormDisplay } from '@utils/getFormDisplay' -import { addHiddenEntity } from '@utils/pokemon/hiddenPokemon' +import { addHiddenEntity } from '@utils/hiddenEntities' const rowClass = { width: 30, fontWeight: 'bold' } diff --git a/src/features/pokestop/PokestopPopup.jsx b/src/features/pokestop/PokestopPopup.jsx index 7523f006b..772356add 100644 --- a/src/features/pokestop/PokestopPopup.jsx +++ b/src/features/pokestop/PokestopPopup.jsx @@ -35,7 +35,7 @@ import { useGetAvailable } from '@hooks/useGetAvailable' import { parseQuestConditions } from '@utils/parseConditions' import { Img } from '@components/Img' import { readableProbability } from '@utils/readableProbability' -import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' +import { addHiddenEntity, showHideSnackbar } from '@utils/hiddenEntities' import { usePokemonBackgroundVisuals, usePokemonBackgroundVisual, diff --git a/src/features/station/StationPopup.jsx b/src/features/station/StationPopup.jsx index d32040747..05eb990f7 100644 --- a/src/features/station/StationPopup.jsx +++ b/src/features/station/StationPopup.jsx @@ -27,7 +27,7 @@ import { Img, PokemonImg } from '@components/Img' import { useFormatStore } from '@store/useFormatStore' import { useRelativeTimer } from '@hooks/useRelativeTime' import { useAnalytics } from '@hooks/useAnalytics' -import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' +import { addHiddenEntity, showHideSnackbar } from '@utils/hiddenEntities' import { BackgroundCard } from '@components/popups/BackgroundCard' import { Title } from '@components/popups/Title' import { diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index d8d9bdc9e..0be0c0b0f 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -20,7 +20,7 @@ import { StatusIcon } from '@components/StatusIcon' import { Title } from '@components/popups/Title' import { getTimeUntil } from '@utils/getTimeUntil' -import { addHiddenEntity, showHideSnackbar } from '@utils/pokemon/hiddenPokemon' +import { addHiddenEntity, showHideSnackbar } from '@utils/hiddenEntities' import { getTappableDisplaySettings } from './displayRules' diff --git a/src/pages/map/components/Container.jsx b/src/pages/map/components/Container.jsx index ffacef9a8..4aad52db4 100644 --- a/src/pages/map/components/Container.jsx +++ b/src/pages/map/components/Container.jsx @@ -9,6 +9,7 @@ import { ScanOnDemand } from '@features/scanner' import { WebhookMarker, WebhookAreaSelection } from '@features/webhooks' import { ActiveWeather } from '@features/weather' import { timeCheck } from '@utils/timeCheck' +import { cleanupHiddenEntities } from '@utils/hiddenEntities' import { Effects } from './Effects' import { DataView } from './Data' @@ -38,6 +39,14 @@ const MAX_BOUNDS = /** @type {[[number, number], [number, number]]} */ ([ export function Container() { const { location, zoom } = useStorage.getState() + // Cleanup hidden entities 15 seconds after map loads + React.useEffect(() => { + const timer = setTimeout(() => { + cleanupHiddenEntities(useMemory.setState) + }, 15000) + return () => clearTimeout(timer) + }, []) + return (