From 7892b6ca8045a00e330e7868fed260f694142ae5 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 24 Sep 2025 16:44:15 -0700 Subject: [PATCH 1/8] feat: persistent popup for gyms --- server/src/models/Gym.js | 25 +++++++++-- src/features/gym/GymTile.jsx | 11 +++++ src/pages/map/components/QueryData.jsx | 61 +++++++++++++++++++++----- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/server/src/models/Gym.js b/server/src/models/Gym.js index ffb49a834..3665499aa 100644 --- a/server/src/models/Gym.js +++ b/server/src/models/Gym.js @@ -83,6 +83,7 @@ class Gym extends Model { onlyGymBadges, onlyBadge, onlyAreas = [], + onlyManualId, } = args.filters const ts = Math.floor(Date.now() / 1000) const query = this.query() @@ -130,9 +131,27 @@ class Gym extends Model { } else if (hideOldGyms) { query.where('updated', '>', ts - gymValidDataLimit * 86400) } - query - .whereBetween(isMad ? 'latitude' : 'lat', [args.minLat, args.maxLat]) - .andWhereBetween(isMad ? 'longitude' : 'lon', [args.minLon, args.maxLon]) + const latCol = isMad ? 'latitude' : 'lat' + const lonCol = isMad ? 'longitude' : 'lon' + const idCol = isMad ? 'gym.gym_id' : 'id' + const manualIds = Array.isArray(onlyManualId) + ? onlyManualId.filter(Boolean) + : onlyManualId || onlyManualId === 0 + ? [onlyManualId] + : [] + + if (manualIds.length) { + query.where((builder) => { + builder + .whereBetween(latCol, [args.minLat, args.maxLat]) + .andWhereBetween(lonCol, [args.minLon, args.maxLon]) + .orWhereIn(idCol, manualIds) + }) + } else { + query + .whereBetween(latCol, [args.minLat, args.maxLat]) + .andWhereBetween(lonCol, [args.minLon, args.maxLon]) + } Gym.onlyValid(query, isMad) const raidBosses = new Set() diff --git a/src/features/gym/GymTile.jsx b/src/features/gym/GymTile.jsx index 139731905..70910b899 100644 --- a/src/features/gym/GymTile.jsx +++ b/src/features/gym/GymTile.jsx @@ -155,6 +155,16 @@ const BaseGymTile = (gym) => { useForcePopup(gym.id, markerRef) useMarkerTimer(timerToDisplay, markerRef, () => setStateChange(!stateChange)) + const handlePopupOpen = React.useCallback(() => { + const { manualParams } = useMemory.getState() + const manualCategory = (manualParams.category || '').toLowerCase() + if ( + manualParams.id !== gym.id || + (manualCategory !== 'gyms' && manualCategory !== 'raids') + ) { + useMemory.setState({ manualParams: { category: 'gyms', id: gym.id } }) + } + }, [gym.id]) if (hasRaid) { sendNotification(`${gym.id}-${hasHatched}`, gym.name, 'raids', { lat: gym.lat, @@ -193,6 +203,7 @@ const BaseGymTile = (gym) => { selectPoi(gym.id, gym.lat, gym.lon) } }, + popupopen: handlePopupOpen, }} > diff --git a/src/pages/map/components/QueryData.jsx b/src/pages/map/components/QueryData.jsx index 81f8a1817..027f96603 100644 --- a/src/pages/map/components/QueryData.jsx +++ b/src/pages/map/components/QueryData.jsx @@ -34,6 +34,20 @@ const userSettingsCategory = (category) => { } } +const normalizeCategory = (category) => { + if (!category) return '' + const lower = category.toLowerCase() + switch (lower) { + case 'raids': + return 'gyms' + case 'lures': + case 'invasions': + return 'pokestops' + default: + return lower + } +} + /** * @template {keyof import('@rm/types').AllFilters} T * @param {import('@rm/types').AllFilters[T]} requestedFilters @@ -105,6 +119,7 @@ function QueryData({ category, timeout }) { const hideList = useMemory((s) => s.hideList) const active = useMemory((s) => s.active) + const manualParams = useMemory((s) => s.manualParams) const userSettings = useStorage( (s) => s.userSettings[userSettingsCategory(category)], @@ -116,13 +131,40 @@ function QueryData({ category, timeout }) { s.filters?.scanAreas?.filter?.areas, ) - const initial = React.useMemo( - () => ({ - ...getQueryArgs(), - filters: trimFilters(filters, userSettings, category, onlyAreas), - }), - [], + const normalizedCategory = React.useMemo( + () => normalizeCategory(category), + [category], ) + const manualId = React.useMemo(() => { + if ( + manualParams?.id === undefined || + manualParams.id === '' || + manualParams.id === null + ) { + return undefined + } + const manualCategory = normalizeCategory(manualParams.category) + return manualCategory === normalizedCategory ? manualParams.id : undefined + }, [manualParams, normalizedCategory]) + + const buildVariables = React.useCallback(() => { + const args = getQueryArgs() + const trimmedFilters = trimFilters( + filters, + userSettings, + category, + onlyAreas, + ) + if (manualId && normalizedCategory === 'gyms') { + trimmedFilters.onlyManualId = manualId + } + return { + ...args, + filters: trimmedFilters, + } + }, [filters, userSettings, category, onlyAreas, manualId, normalizedCategory]) + + const initial = React.useMemo(() => buildVariables(), [buildVariables]) const { data, previousData, error, refetch } = useQuery( Query[category](filters), { @@ -151,10 +193,7 @@ function QueryData({ category, timeout }) { React.useEffect(() => { const refetchData = () => { if (category !== 'scanAreas') { - timeout.current.doRefetch({ - ...getQueryArgs(), - filters: trimFilters(filters, userSettings, category, onlyAreas), - }) + timeout.current.doRefetch(buildVariables()) } } map.on('fetchdata', refetchData) @@ -162,7 +201,7 @@ function QueryData({ category, timeout }) { return () => { map.off('fetchdata', refetchData) } - }, [filters, userSettings, onlyAreas, timeout.current.refetch]) + }, [category, map, buildVariables, timeout]) const errorState = useProcessError(error) From b1838b8d1390d108d559ab5f6778b8e1ae9b29a7 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 24 Sep 2025 17:06:05 -0700 Subject: [PATCH 2/8] chore: first draft for adapting to other popups --- server/src/models/Gym.js | 33 +++-- server/src/models/Nest.js | 15 ++- server/src/models/Pokemon.js | 169 +++++++++++++------------ server/src/models/Pokestop.js | 17 ++- server/src/models/Portal.js | 21 ++- server/src/models/Route.js | 39 +++++- server/src/models/Station.js | 17 ++- server/src/utils/manualFilter.js | 40 ++++++ src/features/gym/GymTile.jsx | 12 +- src/features/nest/NestTile.jsx | 18 ++- src/features/pokemon/PokemonTile.jsx | 3 + src/features/pokestop/PokestopTile.jsx | 3 + src/features/portal/PortalTile.jsx | 3 + src/features/route/RoutePopup.jsx | 4 + src/features/route/RouteTile.jsx | 8 +- src/features/station/StationTile.jsx | 3 + src/hooks/useManualPopupTracker.js | 30 +++++ src/pages/map/components/QueryData.jsx | 25 ++-- src/utils/normalizeCategory.js | 21 +++ 19 files changed, 329 insertions(+), 152 deletions(-) create mode 100644 server/src/utils/manualFilter.js create mode 100644 src/hooks/useManualPopupTracker.js create mode 100644 src/utils/normalizeCategory.js diff --git a/server/src/models/Gym.js b/server/src/models/Gym.js index 3665499aa..c2f3e4830 100644 --- a/server/src/models/Gym.js +++ b/server/src/models/Gym.js @@ -9,6 +9,8 @@ const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') const { state } = require('../services/state') +const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') + const coreFields = [ 'id', 'name', @@ -134,24 +136,19 @@ class Gym extends Model { const latCol = isMad ? 'latitude' : 'lat' const lonCol = isMad ? 'longitude' : 'lon' const idCol = isMad ? 'gym.gym_id' : 'id' - const manualIds = Array.isArray(onlyManualId) - ? onlyManualId.filter(Boolean) - : onlyManualId || onlyManualId === 0 - ? [onlyManualId] - : [] - - if (manualIds.length) { - query.where((builder) => { - builder - .whereBetween(latCol, [args.minLat, args.maxLat]) - .andWhereBetween(lonCol, [args.minLon, args.maxLon]) - .orWhereIn(idCol, manualIds) - }) - } else { - query - .whereBetween(latCol, [args.minLat, args.maxLat]) - .andWhereBetween(lonCol, [args.minLon, args.maxLon]) - } + const manualIds = parseManualIds(onlyManualId) + applyManualIdFilter(query, { + manualIds, + latColumn: latCol, + lonColumn: lonCol, + idColumn: idCol, + bounds: { + minLat: args.minLat, + maxLat: args.maxLat, + minLon: args.minLon, + maxLon: args.maxLon, + }, + }) Gym.onlyValid(query, isMad) const raidBosses = new Set() diff --git a/server/src/models/Nest.js b/server/src/models/Nest.js index 4a7020fe4..41b477aa7 100644 --- a/server/src/models/Nest.js +++ b/server/src/models/Nest.js @@ -5,6 +5,7 @@ const config = require('@rm/config') const { state } = require('../services/state') const { getAreaSql } = require('../utils/getAreaSql') +const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') /** @typedef {Nest & Partial} FullNest */ @@ -27,11 +28,15 @@ class Nest extends Model { static async getAll(perms, args, { polygon }) { const { areaRestrictions } = perms const { minLat, minLon, maxLat, maxLon, filters } = args - const query = this.query() - .select(['*', 'nest_id AS id']) - // .whereNotNull('pokemon_id') - .whereBetween('lat', [minLat, maxLat]) - .andWhereBetween('lon', [minLon, maxLon]) + const manualIds = parseManualIds(filters.onlyManualId) + const query = this.query().select(['*', 'nest_id AS id']) + applyManualIdFilter(query, { + manualIds, + latColumn: 'lat', + lonColumn: 'lon', + idColumn: 'nest_id', + bounds: { minLat, maxLat, minLon, maxLon }, + }) const pokemon = [] if (filters.onlyPokemon) { diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index 692b1bb05..c25f990f0 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -14,6 +14,7 @@ const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') const { filterRTree } = require('../utils/filterRTree') const { fetchJson } = require('../utils/fetchJson') +const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') const { IV_CALC, LEVEL_CALC, @@ -130,6 +131,7 @@ class Pokemon extends Model { const { onlyIvOr, onlyHundoIv, onlyZeroIv, onlyAreas = [] } = args.filters const { hasSize, hasHeight, isMad, mem, secret, httpAuth, pvpV2 } = ctx const { filterMap, globalFilter } = this.getFilters(perms, args, ctx) + const manualIds = parseManualIds(args.filters.onlyManualId) let queryPvp = config .getSafe('api.pvp.leagues') @@ -169,75 +171,77 @@ class Pokemon extends Model { } else { query.select(['*', hasSize && !hasHeight ? 'size AS height' : 'size']) } - query - .where( - isMad ? 'disappear_time' : 'expire_timestamp', - '>=', - isMad ? this.knex().fn.now() : ts, - ) - .andWhereBetween(isMad ? 'pokemon.latitude' : 'lat', [ - args.minLat, - args.maxLat, - ]) - .andWhereBetween(isMad ? 'pokemon.longitude' : 'lon', [ - args.minLon, - args.maxLon, - ]) - .andWhere((ivOr) => { - if (ivs || pvp) { - if (globalFilter.filterKeys.size) { - ivOr.andWhere((pkmn) => { - const keys = globalFilter.keyArray - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i] - switch (key) { - case 'xxs': - case 'xxl': - if (hasSize) { - pkmn.orWhere('pokemon.size', key === 'xxl' ? 5 : 1) - } - break - case 'gender': - pkmn.andWhere('pokemon.gender', onlyIvOr[key]) - break - case 'cp': - case 'level': - case 'atk_iv': - case 'def_iv': - case 'sta_iv': - case 'iv': - if (perms.iv) { - pkmn.andWhereBetween( - isMad ? MAD_KEY_MAP[key] : key, - onlyIvOr[key], - ) - } - break - default: - if ( - perms.pvp && - BASE_KEYS.every((x) => !globalFilter.filterKeys.has(x)) - ) { - // doesn't return everything if only pvp stats for individual pokemon - pkmn.whereNull('pokemon_id') - } - break - } + query.where( + isMad ? 'disappear_time' : 'expire_timestamp', + '>=', + isMad ? this.knex().fn.now() : ts, + ) + applyManualIdFilter(query, { + manualIds, + latColumn: isMad ? 'pokemon.latitude' : 'lat', + lonColumn: isMad ? 'pokemon.longitude' : 'lon', + idColumn: isMad ? 'pokemon.encounter_id' : 'id', + bounds: { + minLat: args.minLat, + maxLat: args.maxLat, + minLon: args.minLon, + maxLon: args.maxLon, + }, + }).andWhere((ivOr) => { + if (ivs || pvp) { + if (globalFilter.filterKeys.size) { + ivOr.andWhere((pkmn) => { + const keys = globalFilter.keyArray + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i] + switch (key) { + case 'xxs': + case 'xxl': + if (hasSize) { + pkmn.orWhere('pokemon.size', key === 'xxl' ? 5 : 1) + } + break + case 'gender': + pkmn.andWhere('pokemon.gender', onlyIvOr[key]) + break + case 'cp': + case 'level': + case 'atk_iv': + case 'def_iv': + case 'sta_iv': + case 'iv': + if (perms.iv) { + pkmn.andWhereBetween( + isMad ? MAD_KEY_MAP[key] : key, + onlyIvOr[key], + ) + } + break + default: + if ( + perms.pvp && + BASE_KEYS.every((x) => !globalFilter.filterKeys.has(x)) + ) { + // doesn't return everything if only pvp stats for individual pokemon + pkmn.whereNull('pokemon_id') + } + break } - }) - } else { - ivOr.whereNull('pokemon_id') - } - ivOr.orWhereIn('pokemon_id', pokemonIds) - ivOr.orWhereIn('pokemon.form', pokemonForms) + } + }) + } else { + ivOr.whereNull('pokemon_id') } - if (onlyZeroIv && ivs) { - ivOr.orWhere(isMad ? raw(IV_CALC) : 'iv', 0) - } - if (onlyHundoIv && ivs) { - ivOr.orWhere(isMad ? raw(IV_CALC) : 'iv', 100) - } - }) + ivOr.orWhereIn('pokemon_id', pokemonIds) + ivOr.orWhereIn('pokemon.form', pokemonForms) + } + if (onlyZeroIv && ivs) { + ivOr.orWhere(isMad ? raw(IV_CALC) : 'iv', 0) + } + if (onlyHundoIv && ivs) { + ivOr.orWhere(isMad ? raw(IV_CALC) : 'iv', 100) + } + }) if (!getAreaSql(query, areaRestrictions, onlyAreas, isMad, 'pokemon')) { return [] } @@ -326,20 +330,23 @@ class Pokemon extends Model { if (isMad) { Pokemon.getMadSql(pvpQuery) } - pvpQuery - .where( - isMad ? 'disappear_time' : 'expire_timestamp', - '>=', - isMad ? this.knex().fn.now() : ts, - ) - .andWhereBetween(isMad ? 'pokemon.latitude' : 'lat', [ - args.minLat, - args.maxLat, - ]) - .andWhereBetween(isMad ? 'pokemon.longitude' : 'lon', [ - args.minLon, - args.maxLon, - ]) + pvpQuery.where( + isMad ? 'disappear_time' : 'expire_timestamp', + '>=', + isMad ? this.knex().fn.now() : ts, + ) + applyManualIdFilter(pvpQuery, { + manualIds, + latColumn: isMad ? 'pokemon.latitude' : 'lat', + lonColumn: isMad ? 'pokemon.longitude' : 'lon', + idColumn: isMad ? 'pokemon.encounter_id' : 'id', + bounds: { + minLat: args.minLat, + maxLat: args.maxLat, + minLon: args.minLon, + maxLon: args.maxLon, + }, + }) if (isMad && listOfIds.length) { pvpQuery.whereRaw( `pokemon.encounter_id NOT IN ( ${listOfIds.join(',')} )`, diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index 05a06a76d..c10003be3 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -6,6 +6,7 @@ const i18next = require('i18next') const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') +const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') const { getUserMidnight } = require('../utils/getClientTime') const { state } = require('../services/state') @@ -138,6 +139,7 @@ class Pokestop extends Model { const ts = Math.floor(Date.now() / 1000) const { queryLimits, stopValidDataLimit, hideOldPokestops } = config.getSafe('api') + const manualIds = parseManualIds(args.filters.onlyManualId) const { lures: lurePerms, @@ -184,9 +186,18 @@ class Pokestop extends Model { query.where('pokestop.updated', '>', ts - stopValidDataLimit * 86400) } Pokestop.joinIncident(query, hasMultiInvasions, isMad, multiInvasionMs) - query - .whereBetween(isMad ? 'latitude' : 'lat', [args.minLat, args.maxLat]) - .andWhereBetween(isMad ? 'longitude' : 'lon', [args.minLon, args.maxLon]) + applyManualIdFilter(query, { + manualIds, + latColumn: isMad ? 'latitude' : 'lat', + lonColumn: isMad ? 'longitude' : 'lon', + idColumn: isMad ? 'pokestop.pokestop_id' : 'pokestop.id', + bounds: { + minLat: args.minLat, + maxLat: args.maxLat, + minLon: args.minLon, + maxLon: args.maxLon, + }, + }) if (!getAreaSql(query, areaRestrictions, onlyAreas, isMad)) { return [] diff --git a/server/src/models/Portal.js b/server/src/models/Portal.js index 79a9d3aa1..9ece40081 100644 --- a/server/src/models/Portal.js +++ b/server/src/models/Portal.js @@ -3,6 +3,7 @@ const { Model } = require('objection') const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') +const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') class Portal extends Model { static get tableName() { @@ -28,14 +29,20 @@ class Portal extends Model { maxLat, maxLon, } = args + const manualIds = parseManualIds(args.filters.onlyManualId) const query = this.query() - .whereBetween('lat', [minLat, maxLat]) - .andWhereBetween('lon', [minLon, maxLon]) - .andWhere( - 'updated', - '>', - Date.now() / 1000 - portalUpdateLimit * 60 * 60 * 24, - ) + applyManualIdFilter(query, { + manualIds, + latColumn: 'lat', + lonColumn: 'lon', + idColumn: 'id', + bounds: { minLat, maxLat, minLon, maxLon }, + }) + query.andWhere( + 'updated', + '>', + Date.now() / 1000 - portalUpdateLimit * 60 * 60 * 24, + ) if (!getAreaSql(query, areaRestrictions, onlyAreas)) { return [] } diff --git a/server/src/models/Route.js b/server/src/models/Route.js index e4d696a40..dac1876eb 100644 --- a/server/src/models/Route.js +++ b/server/src/models/Route.js @@ -4,6 +4,7 @@ const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') const { getEpoch } = require('../utils/getClientTime') +const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') const GET_ALL_SELECT = /** @type {const} */ ([ 'id', @@ -44,6 +45,7 @@ class Route extends Model { const ts = getEpoch() - config.getSafe('api.routeUpdateLimit') * 24 * 60 * 60 const distanceInMeters = (onlyDistance || [0.5, 100]).map((x) => x * 1000) + const manualIds = parseManualIds(args.filters.onlyManualId) const startLatitude = isMad ? 'start_poi_latitude' : 'start_lat' const startLongitude = isMad ? 'start_poi_longitude' : 'start_lon' @@ -51,10 +53,23 @@ class Route extends Model { const endLatitude = isMad ? 'end_poi_latitude' : 'end_lat' const endLongitude = isMad ? 'end_poi_longitude' : 'end_lon' - const query = this.query() - .select(isMad ? GET_MAD_ALL_SELECT : GET_ALL_SELECT) - .whereBetween(startLatitude, [args.minLat, args.maxLat]) - .andWhereBetween(startLongitude, [args.minLon, args.maxLon]) + const idColumn = isMad ? 'route_id' : 'id' + const query = this.query().select( + isMad ? GET_MAD_ALL_SELECT : GET_ALL_SELECT, + ) + applyManualIdFilter(query, { + manualIds, + latColumn: startLatitude, + lonColumn: startLongitude, + idColumn, + bounds: { + minLat: args.minLat, + maxLat: args.maxLat, + minLon: args.minLon, + maxLon: args.maxLon, + }, + }) + query .andWhereBetween(distanceMeters, distanceInMeters) .andWhere((builder) => { builder.where( @@ -68,9 +83,19 @@ class Route extends Model { }) .union((qb) => { qb.select(isMad ? GET_MAD_ALL_SELECT : GET_ALL_SELECT) - .whereBetween(endLatitude, [args.minLat, args.maxLat]) - .andWhereBetween(endLongitude, [args.minLon, args.maxLon]) - .andWhereBetween(distanceMeters, distanceInMeters) + applyManualIdFilter(qb, { + manualIds, + latColumn: endLatitude, + lonColumn: endLongitude, + idColumn, + bounds: { + minLat: args.minLat, + maxLat: args.maxLat, + minLon: args.minLon, + maxLon: args.maxLon, + }, + }) + qb.andWhereBetween(distanceMeters, distanceInMeters) .andWhere((builder) => { builder.where( isMad ? raw('UNIX_TIMESTAMP(last_updated)') : 'updated', diff --git a/server/src/models/Station.js b/server/src/models/Station.js index 46172f0ab..2306bc413 100644 --- a/server/src/models/Station.js +++ b/server/src/models/Station.js @@ -4,6 +4,7 @@ const config = require('@rm/config') const i18next = require('i18next') const { getAreaSql } = require('../utils/getAreaSql') +const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') const { getEpoch } = require('../utils/getClientTime') const { state } = require('../services/state') @@ -30,6 +31,7 @@ class Station extends Model { onlyGmaxStationed, } = args.filters const ts = getEpoch() + const manualIds = parseManualIds(args.filters.onlyManualId) const select = [ 'id', @@ -42,8 +44,19 @@ class Station extends Model { ] const query = this.query() - .whereBetween('lat', [args.minLat, args.maxLat]) - .andWhereBetween('lon', [args.minLon, args.maxLon]) + applyManualIdFilter(query, { + manualIds, + latColumn: 'lat', + lonColumn: 'lon', + idColumn: 'id', + bounds: { + minLat: args.minLat, + maxLat: args.maxLat, + minLon: args.minLon, + maxLon: args.maxLon, + }, + }) + query .andWhere('end_time', '>', ts) .andWhere( 'updated', diff --git a/server/src/utils/manualFilter.js b/server/src/utils/manualFilter.js new file mode 100644 index 000000000..5fff702da --- /dev/null +++ b/server/src/utils/manualFilter.js @@ -0,0 +1,40 @@ +// @ts-check + +/** + * @param {unknown} value + * @returns {(string | number)[]} + */ +function parseManualIds(value) { + if (Array.isArray(value)) { + return value.filter((id) => id !== undefined && id !== null && id !== '') + } + return value !== undefined && value !== null && value !== '' ? [value] : [] +} + +/** + * @param {import('objection').QueryBuilder} query + * @param {{ + * manualIds: (string | number)[], + * latColumn: string, + * lonColumn: string, + * idColumn: string, + * bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number }, + * }} options + */ +function applyManualIdFilter(query, options) { + const { manualIds, latColumn, lonColumn, idColumn, bounds } = options + if (manualIds.length) { + query.where((builder) => { + builder + .whereBetween(latColumn, [bounds.minLat, bounds.maxLat]) + .andWhereBetween(lonColumn, [bounds.minLon, bounds.maxLon]) + .orWhereIn(idColumn, manualIds) + }) + } else { + query + .whereBetween(latColumn, [bounds.minLat, bounds.maxLat]) + .andWhereBetween(lonColumn, [bounds.minLon, bounds.maxLon]) + } +} + +module.exports = { parseManualIds, applyManualIdFilter } diff --git a/src/features/gym/GymTile.jsx b/src/features/gym/GymTile.jsx index 70910b899..61c5333ef 100644 --- a/src/features/gym/GymTile.jsx +++ b/src/features/gym/GymTile.jsx @@ -9,6 +9,7 @@ import { basicEqualFn, useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { useOpacity } from '@hooks/useOpacity' import { useForcePopup } from '@hooks/useForcePopup' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { sendNotification } from '@services/desktopNotification' import { TooltipWrapper } from '@components/ToolTipWrapper' import { getTimeUntil } from '@utils/getTimeUntil' @@ -155,16 +156,7 @@ const BaseGymTile = (gym) => { useForcePopup(gym.id, markerRef) useMarkerTimer(timerToDisplay, markerRef, () => setStateChange(!stateChange)) - const handlePopupOpen = React.useCallback(() => { - const { manualParams } = useMemory.getState() - const manualCategory = (manualParams.category || '').toLowerCase() - if ( - manualParams.id !== gym.id || - (manualCategory !== 'gyms' && manualCategory !== 'raids') - ) { - useMemory.setState({ manualParams: { category: 'gyms', id: gym.id } }) - } - }, [gym.id]) + const handlePopupOpen = useManualPopupTracker('gyms', gym.id) if (hasRaid) { sendNotification(`${gym.id}-${hasHatched}`, gym.name, 'raids', { lat: gym.lat, diff --git a/src/features/nest/NestTile.jsx b/src/features/nest/NestTile.jsx index 69bc13f1b..a6ec1ee41 100644 --- a/src/features/nest/NestTile.jsx +++ b/src/features/nest/NestTile.jsx @@ -7,6 +7,7 @@ import { GeoJSON, Marker, Popup } from 'react-leaflet' import { basicEqualFn, useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { useForcePopup } from '@hooks/useForcePopup' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { nestMarker } from './nestMarker' import { NestPopup } from './NestPopup' @@ -19,6 +20,7 @@ import { NestPopup } from './NestPopup' const BaseNestTile = (nest) => { const recent = Date.now() / 1000 - nest.updated < 172800000 const internalId = `${nest.pokemon_id}-${nest.pokemon_form}` + const handlePopupOpen = useManualPopupTracker('nests', nest.id) const size = useStorage( (s) => s.filters.nests.filter[internalId]?.size || 'md', @@ -39,13 +41,17 @@ const BaseNestTile = (nest) => { iconSize={iconSize} recent={recent} nest={nest} + eventHandlers={{ popupopen: handlePopupOpen }} > )} - + @@ -107,10 +113,10 @@ const NestMarker = ({ /** * - * @param {{ polygon_path: string, children?: React.ReactNode }} props + * @param {{ polygon_path: string, children?: React.ReactNode, eventHandlers?: import('leaflet').LeafletEventHandlerFnMap }} props * @returns */ -const NestGeoJSON = ({ polygon_path, children }) => { +const NestGeoJSON = ({ polygon_path, eventHandlers, children }) => { const showPolygons = useStorage((s) => s.filters.nests.polygons) const geometry = React.useMemo(() => { @@ -126,7 +132,11 @@ const NestGeoJSON = ({ polygon_path, children }) => { if (!showPolygons) { return null } - return {children} + return ( + + {children} + + ) } export const NestTile = React.memo( diff --git a/src/features/pokemon/PokemonTile.jsx b/src/features/pokemon/PokemonTile.jsx index 18d4772aa..530b128bc 100644 --- a/src/features/pokemon/PokemonTile.jsx +++ b/src/features/pokemon/PokemonTile.jsx @@ -11,6 +11,7 @@ import { basicEqualFn, useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { useOpacity } from '@hooks/useOpacity' import { useForcePopup } from '@hooks/useForcePopup' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { sendNotification } from '@services/desktopNotification' import { useMapStore } from '@store/useMapStore' import { TooltipWrapper } from '@components/ToolTipWrapper' @@ -138,6 +139,7 @@ const BasePokemonTile = (pkmn) => { useForcePopup(pkmn.id, markerRef) useMarkerTimer(pkmn.expire_timestamp, markerRef) + const handlePopupOpen = useManualPopupTracker('pokemon', pkmn.id) sendNotification( pkmn.id, `${t(`poke_${pkmn.pokemon_id}`)}${ @@ -188,6 +190,7 @@ const BasePokemonTile = (pkmn) => { }) : basicPokemonMarker({ iconUrl, iconSize }) } + eventHandlers={{ popupopen: handlePopupOpen }} > diff --git a/src/features/pokestop/PokestopTile.jsx b/src/features/pokestop/PokestopTile.jsx index 6db1bc190..8e64e49a7 100644 --- a/src/features/pokestop/PokestopTile.jsx +++ b/src/features/pokestop/PokestopTile.jsx @@ -8,6 +8,7 @@ import { basicEqualFn, useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { useRouteStore, resolveRoutePoiKey } from '@features/route' import { useForcePopup } from '@hooks/useForcePopup' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { TooltipWrapper } from '@components/ToolTipWrapper' import { PokestopPopup } from './PokestopPopup' @@ -130,6 +131,7 @@ const BasePokestopTile = (pokestop) => { useMarkerTimer(timers.length ? Math.min(...timers) : null, markerRef, () => setStateChange(!stateChange), ) + const handlePopupOpen = useManualPopupTracker('pokestops', pokestop.id) const icon = usePokestopMarker({ hasQuest, @@ -150,6 +152,7 @@ const BasePokestopTile = (pokestop) => { selectPoi(pokestop.id, pokestop.lat, pokestop.lon) } }, + popupopen: handlePopupOpen, }} > diff --git a/src/features/portal/PortalTile.jsx b/src/features/portal/PortalTile.jsx index ac19de2f2..dcb64595e 100644 --- a/src/features/portal/PortalTile.jsx +++ b/src/features/portal/PortalTile.jsx @@ -5,6 +5,7 @@ import { Circle, Popup } from 'react-leaflet' import { useStorage } from '@store/useStorage' import { useForcePopup } from '@hooks/useForcePopup' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { PortalPopup } from './PortalPopup' @@ -22,6 +23,7 @@ const BasePortalTile = (portal) => { ) useForcePopup(portal.id, markerRef) + const handlePopupOpen = useManualPopupTracker('portals', portal.id) return ( { fillOpacity={0.25} color={color} fillColor={color} + eventHandlers={{ popupopen: handlePopupOpen }} > diff --git a/src/features/route/RoutePopup.jsx b/src/features/route/RoutePopup.jsx index c88732a48..09cc7b068 100644 --- a/src/features/route/RoutePopup.jsx +++ b/src/features/route/RoutePopup.jsx @@ -29,6 +29,7 @@ import { Title } from '@components/popups/Title' import { Timer } from '@components/popups/Timer' import { Navigation } from '@components/popups/Navigation' import { Notification } from '@components/Notification' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { useFormatDistance } from './useFormatDistance' @@ -370,6 +371,8 @@ export function RoutePopup({ end, inline = false, ...props }) { return content } + const handlePopupOpen = useManualPopupTracker('routes', props.id) + return ( { @@ -377,6 +380,7 @@ export function RoutePopup({ end, inline = false, ...props }) { getRoute() } }} + eventHandlers={{ popupopen: handlePopupOpen }} > {content} diff --git a/src/features/route/RouteTile.jsx b/src/features/route/RouteTile.jsx index dd95c6297..f7a63a93c 100644 --- a/src/features/route/RouteTile.jsx +++ b/src/features/route/RouteTile.jsx @@ -6,6 +6,7 @@ import 'leaflet-arrowheads' import { darken } from '@mui/material/styles' import { useForcePopup } from '@hooks/useForcePopup' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { routeMarker } from './routeMarker' import { ROUTE_MARKER_PANE } from './constants' @@ -105,6 +106,7 @@ const BaseRouteTile = ({ route, orientation = 'forward' }) => { }, }) useForcePopup(displayRoute.id, markerRef) + const handleRoutePopupOpen = useManualPopupTracker('routes', displayRoute.id) React.useEffect(() => { setLinePopup(null) @@ -194,7 +196,10 @@ const BaseRouteTile = ({ route, orientation = 'forward' }) => { } pane={ROUTE_MARKER_PANE} eventHandlers={{ - popupopen: () => setClicked(true), + popupopen: () => { + setClicked(true) + handleRoutePopupOpen() + }, popupclose: () => setClicked(false), mouseover: () => { if (lineRef.current) { @@ -280,6 +285,7 @@ const BaseRouteTile = ({ route, orientation = 'forward' }) => { eventHandlers={{ remove: () => setLinePopup(null), close: () => setLinePopup(null), + popupopen: handleRoutePopupOpen, }} > diff --git a/src/features/station/StationTile.jsx b/src/features/station/StationTile.jsx index b0638cdae..5da5464a2 100644 --- a/src/features/station/StationTile.jsx +++ b/src/features/station/StationTile.jsx @@ -7,6 +7,7 @@ import { useMarkerTimer } from '@hooks/useMarkerTimer' import { basicEqualFn, useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { useForcePopup } from '@hooks/useForcePopup' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { TooltipWrapper } from '@components/ToolTipWrapper' import { StationPopup } from './StationPopup' @@ -52,12 +53,14 @@ const BaseStationTile = (station) => { useMarkerTimer(timers.length ? Math.min(...timers) : null, markerRef, () => setStateChange(!stateChange), ) + const handlePopupOpen = useManualPopupTracker('stations', station.id) return ( diff --git a/src/hooks/useManualPopupTracker.js b/src/hooks/useManualPopupTracker.js new file mode 100644 index 000000000..79c203c0d --- /dev/null +++ b/src/hooks/useManualPopupTracker.js @@ -0,0 +1,30 @@ +// @ts-check +import { useCallback } from 'react' + +import { useMemory } from '@store/useMemory' +import { normalizeCategory } from '@utils/normalizeCategory' + +/** + * Provide a stable handler that keeps manual popup tracking in sync with UI. + * + * @param {string} category + * @param {string | number} id + * @returns {() => void} + */ +export function useManualPopupTracker(category, id) { + const normalizedCategory = normalizeCategory(category) + return useCallback(() => { + if (id === undefined || id === null || id === '') return + const { manualParams } = useMemory.getState() + const currentCategory = normalizeCategory(manualParams.category) + if (manualParams.id === id && currentCategory === normalizedCategory) { + return + } + useMemory.setState({ + manualParams: { + category, + id, + }, + }) + }, [category, id, normalizedCategory]) +} diff --git a/src/pages/map/components/QueryData.jsx b/src/pages/map/components/QueryData.jsx index 027f96603..938619def 100644 --- a/src/pages/map/components/QueryData.jsx +++ b/src/pages/map/components/QueryData.jsx @@ -14,6 +14,7 @@ import { GenerateCells } from '@features/s2cell' import { RouteLayer } from '@features/route' import { useAnalytics } from '@hooks/useAnalytics' import { useProcessError } from '@hooks/useProcessError' +import { normalizeCategory } from '@utils/normalizeCategory' import { Clustering } from './Clustering' import { TILES } from '../tileObject' @@ -34,19 +35,15 @@ const userSettingsCategory = (category) => { } } -const normalizeCategory = (category) => { - if (!category) return '' - const lower = category.toLowerCase() - switch (lower) { - case 'raids': - return 'gyms' - case 'lures': - case 'invasions': - return 'pokestops' - default: - return lower - } -} +const MANUAL_ID_CATEGORIES = new Set([ + 'gyms', + 'pokestops', + 'pokemon', + 'nests', + 'portals', + 'stations', + 'routes', +]) /** * @template {keyof import('@rm/types').AllFilters} T @@ -155,7 +152,7 @@ function QueryData({ category, timeout }) { category, onlyAreas, ) - if (manualId && normalizedCategory === 'gyms') { + if (manualId && MANUAL_ID_CATEGORIES.has(normalizedCategory)) { trimmedFilters.onlyManualId = manualId } return { diff --git a/src/utils/normalizeCategory.js b/src/utils/normalizeCategory.js new file mode 100644 index 000000000..2841e327d --- /dev/null +++ b/src/utils/normalizeCategory.js @@ -0,0 +1,21 @@ +// @ts-check + +/** + * Normalize category identifiers so data and UI share consistent lookups. + * + * @param {string} category + * @returns {string} + */ +export function normalizeCategory(category) { + if (!category) return '' + const lower = category.toLowerCase() + switch (lower) { + case 'raids': + return 'gyms' + case 'lures': + case 'invasions': + return 'pokestops' + default: + return lower + } +} From bbce2f9de166a78dda081e3fd872290ec812df93 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 24 Sep 2025 17:16:34 -0700 Subject: [PATCH 3/8] fix: pokestop persistent popup --- server/src/models/Pokestop.js | 2 +- src/pages/map/components/Clustering.jsx | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index c10003be3..40ac5291c 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -190,7 +190,7 @@ class Pokestop extends Model { manualIds, latColumn: isMad ? 'latitude' : 'lat', lonColumn: isMad ? 'longitude' : 'lon', - idColumn: isMad ? 'pokestop.pokestop_id' : 'pokestop.id', + idColumn: isMad ? 'pokestop.pokestop_id' : 'id', bounds: { minLat: args.minLat, maxLat: args.maxLat, diff --git a/src/pages/map/components/Clustering.jsx b/src/pages/map/components/Clustering.jsx index 46c36f975..8b52e9280 100644 --- a/src/pages/map/components/Clustering.jsx +++ b/src/pages/map/components/Clustering.jsx @@ -7,6 +7,7 @@ import { marker, divIcon, point } from 'leaflet' import { useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { Notification } from '@components/Notification' +import { normalizeCategory } from '@utils/normalizeCategory' const IGNORE_CLUSTERING = new Set([ 'devices', @@ -50,6 +51,13 @@ export function Clustering({ category, children }) { const userCluster = useStorage( (s) => s.userSettings[category]?.clustering || false, ) + const manualParams = useMemory((s) => s.manualParams) + const manualKey = React.useMemo(() => { + const normalized = normalizeCategory(manualParams.category) + return normalized === category && manualParams.id !== undefined + ? `${manualParams.id}` + : null + }, [manualParams, category]) const { config: { clustering, @@ -133,6 +141,9 @@ export function Clustering({ category, children }) { newMarkers.add(cluster.id) } } + if (manualKey) { + newMarkers.add(manualKey) + } // @ts-ignore featureRef?.current?.addData(newClusters) setMarkers(newMarkers) From f07d2da528f0fd6e8076c271b904b2c2959d4425 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 24 Sep 2025 17:48:41 -0700 Subject: [PATCH 4/8] fix: pokemon persistent popup --- server/src/models/Pokemon.js | 65 +++++++++++++++++++++++-- src/pages/map/components/Clustering.jsx | 23 ++++++--- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index c25f990f0..c123f9377 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -187,7 +187,8 @@ class Pokemon extends Model { minLon: args.minLon, maxLon: args.maxLon, }, - }).andWhere((ivOr) => { + }) + query.andWhere((ivOr) => { if (ivs || pvp) { if (globalFilter.filterKeys.size) { ivOr.andWhere((pkmn) => { @@ -241,6 +242,9 @@ class Pokemon extends Model { if (onlyHundoIv && ivs) { ivOr.orWhere(isMad ? raw(IV_CALC) : 'iv', 100) } + if (manualIds.length) { + ivOr.orWhereIn(isMad ? 'pokemon.encounter_id' : 'id', manualIds) + } }) if (!getAreaSql(query, areaRestrictions, onlyAreas, isMad, 'pokemon')) { return [] @@ -276,7 +280,7 @@ class Pokemon extends Model { filters.push({ iv: { min: 100, max: 100 }, pokemon: globalPokes }) } /** @type {import("@rm/types").Pokemon[]} */ - const results = await this.evalQuery( + let results = await this.evalQuery( mem ? `${mem}/api/pokemon/v2/scan` : null, mem ? JSON.stringify({ @@ -297,6 +301,34 @@ class Pokemon extends Model { httpAuth, ) + if (mem && manualIds.length) { + const loadedIds = Array.isArray(results) + ? new Set(results.map((pkmn) => `${pkmn.id}`)) + : new Set() + const missingManuals = manualIds.filter((id) => !loadedIds.has(`${id}`)) + if (missingManuals.length) { + const manualResults = await Promise.all( + missingManuals.map((id) => + this.evalQuery( + `${mem}/api/pokemon/id/${id}`, + null, + 'GET', + secret, + httpAuth, + ).catch(() => null), + ), + ) + const validManuals = manualResults.filter(Boolean) + results = Array.isArray(results) + ? [...results, ...validManuals] + : validManuals + } + } + + if (!Array.isArray(results)) { + results = [] + } + const finalResults = [] const pvpResults = [] const listOfIds = [] @@ -347,6 +379,11 @@ class Pokemon extends Model { maxLon: args.maxLon, }, }) + if (manualIds.length) { + pvpQuery.andWhere((builder) => { + builder.orWhereIn(isMad ? 'pokemon.encounter_id' : 'id', manualIds) + }) + } if (isMad && listOfIds.length) { pvpQuery.whereRaw( `pokemon.encounter_id NOT IN ( ${listOfIds.join(',')} )`, @@ -696,7 +733,7 @@ class Pokemon extends Model { if ((perms.iv || perms.pvp) && mem) filters.push(...globalFilter.buildApiFilter()) - const results = await this.evalQuery( + let results = await this.evalQuery( mem ? `${mem}/api/pokemon/v2/scan` : null, mem ? JSON.stringify({ @@ -716,6 +753,28 @@ class Pokemon extends Model { secret, httpAuth, ) + + if (mem && args.filters.onlyManualId) { + const manualIds = parseManualIds(args.filters.onlyManualId) + if (manualIds.length) { + const loaded = new Set(results.map((pkmn) => `${pkmn.id}`)) + const missingManuals = manualIds.filter((id) => !loaded.has(`${id}`)) + if (missingManuals.length) { + const manualResults = await Promise.all( + missingManuals.map((id) => + this.evalQuery( + `${mem}/api/pokemon/id/${id}`, + null, + 'GET', + secret, + httpAuth, + ).catch(() => null), + ), + ) + results = [...results, ...manualResults.filter((pkmn) => pkmn)] + } + } + } const filtered = results.filter( (item) => !mem || diff --git a/src/pages/map/components/Clustering.jsx b/src/pages/map/components/Clustering.jsx index 8b52e9280..6376074e1 100644 --- a/src/pages/map/components/Clustering.jsx +++ b/src/pages/map/components/Clustering.jsx @@ -141,9 +141,7 @@ export function Clustering({ category, children }) { newMarkers.add(cluster.id) } } - if (manualKey) { - newMarkers.add(manualKey) - } + if (manualKey) newMarkers.add(manualKey) // @ts-ignore featureRef?.current?.addData(newClusters) setMarkers(newMarkers) @@ -155,12 +153,25 @@ export function Clustering({ category, children }) { } }, [children, featureRef, superCluster]) + const clustered = React.useMemo(() => { + const requiresFilter = children.length > rules.forcedLimit || userCluster + if (!requiresFilter) return children + const base = children.filter((x) => x && markers.has(x.key)) + if (manualKey) { + const manualChild = children.find( + (child) => child && child.key === manualKey, + ) + if (manualChild && !base.includes(manualChild)) { + return [...base, manualChild] + } + } + return base + }, [children, markers, manualKey, rules.forcedLimit, userCluster]) + return ( <> - {children.length > rules.forcedLimit || userCluster - ? children.filter((x) => x && markers.has(x.key)) - : children} + {clustered} {limitHit && ( Date: Wed, 24 Sep 2025 18:05:19 -0700 Subject: [PATCH 5/8] fix: ai slop --- server/src/models/Pokemon.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index c123f9377..616e5ed57 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -379,11 +379,6 @@ class Pokemon extends Model { maxLon: args.maxLon, }, }) - if (manualIds.length) { - pvpQuery.andWhere((builder) => { - builder.orWhereIn(isMad ? 'pokemon.encounter_id' : 'id', manualIds) - }) - } if (isMad && listOfIds.length) { pvpQuery.whereRaw( `pokemon.encounter_id NOT IN ( ${listOfIds.join(',')} )`, From 8aa97caab6d84e2698b1f54652a6e40d8e52fa1b Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 24 Sep 2025 19:17:14 -0700 Subject: [PATCH 6/8] fix: more ai slop --- server/src/models/Gym.js | 10 ++- server/src/models/Nest.js | 10 ++- server/src/models/Pokemon.js | 103 ++++++++++++++++++------------- server/src/models/Pokestop.js | 10 ++- server/src/models/Portal.js | 10 ++- server/src/models/Route.js | 12 ++-- server/src/models/Station.js | 10 ++- server/src/utils/manualFilter.js | 39 +++++++----- 8 files changed, 125 insertions(+), 79 deletions(-) diff --git a/server/src/models/Gym.js b/server/src/models/Gym.js index c2f3e4830..9761e7011 100644 --- a/server/src/models/Gym.js +++ b/server/src/models/Gym.js @@ -9,7 +9,7 @@ const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') const { state } = require('../services/state') -const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') +const { applyManualIdFilter } = require('../utils/manualFilter') const coreFields = [ 'id', @@ -136,9 +136,13 @@ class Gym extends Model { const latCol = isMad ? 'latitude' : 'lat' const lonCol = isMad ? 'longitude' : 'lon' const idCol = isMad ? 'gym.gym_id' : 'id' - const manualIds = parseManualIds(onlyManualId) + const manualId = + typeof onlyManualId === 'string' || typeof onlyManualId === 'number' + ? onlyManualId + : null + applyManualIdFilter(query, { - manualIds, + manualId, latColumn: latCol, lonColumn: lonCol, idColumn: idCol, diff --git a/server/src/models/Nest.js b/server/src/models/Nest.js index 41b477aa7..144bf839b 100644 --- a/server/src/models/Nest.js +++ b/server/src/models/Nest.js @@ -5,7 +5,7 @@ const config = require('@rm/config') const { state } = require('../services/state') const { getAreaSql } = require('../utils/getAreaSql') -const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') +const { applyManualIdFilter } = require('../utils/manualFilter') /** @typedef {Nest & Partial} FullNest */ @@ -28,10 +28,14 @@ class Nest extends Model { static async getAll(perms, args, { polygon }) { const { areaRestrictions } = perms const { minLat, minLon, maxLat, maxLon, filters } = args - const manualIds = parseManualIds(filters.onlyManualId) + const manualId = + typeof filters.onlyManualId === 'string' || + typeof filters.onlyManualId === 'number' + ? filters.onlyManualId + : null const query = this.query().select(['*', 'nest_id AS id']) applyManualIdFilter(query, { - manualIds, + manualId, latColumn: 'lat', lonColumn: 'lon', idColumn: 'nest_id', diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index 616e5ed57..b620b155d 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -14,7 +14,7 @@ const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') const { filterRTree } = require('../utils/filterRTree') const { fetchJson } = require('../utils/fetchJson') -const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') +const { applyManualIdFilter } = require('../utils/manualFilter') const { IV_CALC, LEVEL_CALC, @@ -131,7 +131,6 @@ class Pokemon extends Model { const { onlyIvOr, onlyHundoIv, onlyZeroIv, onlyAreas = [] } = args.filters const { hasSize, hasHeight, isMad, mem, secret, httpAuth, pvpV2 } = ctx const { filterMap, globalFilter } = this.getFilters(perms, args, ctx) - const manualIds = parseManualIds(args.filters.onlyManualId) let queryPvp = config .getSafe('api.pvp.leagues') @@ -148,8 +147,16 @@ class Pokemon extends Model { if (!noPokemonSelect) return [] } + const manualIdRaw = + typeof args.filters.onlyManualId === 'string' || + typeof args.filters.onlyManualId === 'number' + ? args.filters.onlyManualId + : null + const query = this.query() + let manualId = null + const pokemonIds = [] const pokemonForms = [] Object.values(filterMap).forEach((filter) => { @@ -176,8 +183,8 @@ class Pokemon extends Model { '>=', isMad ? this.knex().fn.now() : ts, ) - applyManualIdFilter(query, { - manualIds, + manualId = applyManualIdFilter(query, { + manualId: manualIdRaw, latColumn: isMad ? 'pokemon.latitude' : 'lat', lonColumn: isMad ? 'pokemon.longitude' : 'lon', idColumn: isMad ? 'pokemon.encounter_id' : 'id', @@ -242,13 +249,15 @@ class Pokemon extends Model { if (onlyHundoIv && ivs) { ivOr.orWhere(isMad ? raw(IV_CALC) : 'iv', 100) } - if (manualIds.length) { - ivOr.orWhereIn(isMad ? 'pokemon.encounter_id' : 'id', manualIds) + if (manualId !== null) { + ivOr.orWhereIn(isMad ? 'pokemon.encounter_id' : 'id', [manualId]) } }) if (!getAreaSql(query, areaRestrictions, onlyAreas, isMad, 'pokemon')) { return [] } + } else { + manualId = manualIdRaw } const filters = mem @@ -301,27 +310,23 @@ class Pokemon extends Model { httpAuth, ) - if (mem && manualIds.length) { + if (mem && manualId !== null) { const loadedIds = Array.isArray(results) ? new Set(results.map((pkmn) => `${pkmn.id}`)) : new Set() - const missingManuals = manualIds.filter((id) => !loadedIds.has(`${id}`)) - if (missingManuals.length) { - const manualResults = await Promise.all( - missingManuals.map((id) => - this.evalQuery( - `${mem}/api/pokemon/id/${id}`, - null, - 'GET', - secret, - httpAuth, - ).catch(() => null), - ), - ) - const validManuals = manualResults.filter(Boolean) - results = Array.isArray(results) - ? [...results, ...validManuals] - : validManuals + if (!loadedIds.has(`${manualId}`)) { + const manualResult = await this.evalQuery( + `${mem}/api/pokemon/id/${manualId}`, + null, + 'GET', + secret, + httpAuth, + ).catch(() => null) + if (manualResult) { + results = Array.isArray(results) + ? [...results, manualResult] + : [manualResult] + } } } @@ -368,7 +373,7 @@ class Pokemon extends Model { isMad ? this.knex().fn.now() : ts, ) applyManualIdFilter(pvpQuery, { - manualIds, + manualId, latColumn: isMad ? 'pokemon.latitude' : 'lat', lonColumn: isMad ? 'pokemon.longitude' : 'lon', idColumn: isMad ? 'pokemon.encounter_id' : 'id', @@ -682,6 +687,11 @@ class Pokemon extends Model { const { isMad, hasSize, hasHeight, mem, secret, httpAuth } = ctx const ts = Math.floor(Date.now() / 1000) const { filterMap, globalFilter } = this.getFilters(perms, args, ctx) + const manualIdRaw = + typeof args.filters.onlyManualId === 'string' || + typeof args.filters.onlyManualId === 'number' + ? args.filters.onlyManualId + : null const queryLimits = config.getSafe('api.queryLimits') if (!perms.iv && !perms.pvp) { @@ -721,7 +731,18 @@ class Pokemon extends Model { ) { return [] } - + const manualId = applyManualIdFilter(query, { + manualId: manualIdRaw, + latColumn: isMad ? 'pokemon.latitude' : 'lat', + lonColumn: isMad ? 'pokemon.longitude' : 'lon', + idColumn: isMad ? 'pokemon.encounter_id' : 'id', + bounds: { + minLat: args.minLat, + maxLat: args.maxLat, + minLon: args.minLon, + maxLon: args.maxLon, + }, + }) const filters = mem ? Object.values(filterMap).flatMap((filter) => filter.buildApiFilter()) : [] @@ -749,24 +770,18 @@ class Pokemon extends Model { httpAuth, ) - if (mem && args.filters.onlyManualId) { - const manualIds = parseManualIds(args.filters.onlyManualId) - if (manualIds.length) { - const loaded = new Set(results.map((pkmn) => `${pkmn.id}`)) - const missingManuals = manualIds.filter((id) => !loaded.has(`${id}`)) - if (missingManuals.length) { - const manualResults = await Promise.all( - missingManuals.map((id) => - this.evalQuery( - `${mem}/api/pokemon/id/${id}`, - null, - 'GET', - secret, - httpAuth, - ).catch(() => null), - ), - ) - results = [...results, ...manualResults.filter((pkmn) => pkmn)] + if (mem && manualId !== null) { + const loaded = new Set(results.map((pkmn) => `${pkmn.id}`)) + if (!loaded.has(`${manualId}`)) { + const manualResult = await this.evalQuery( + `${mem}/api/pokemon/id/${manualId}`, + null, + 'GET', + secret, + httpAuth, + ).catch(() => null) + if (manualResult) { + results = [...results, manualResult] } } } diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index 40ac5291c..18b874dfc 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -6,7 +6,7 @@ const i18next = require('i18next') const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') -const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') +const { applyManualIdFilter } = require('../utils/manualFilter') const { getUserMidnight } = require('../utils/getClientTime') const { state } = require('../services/state') @@ -139,7 +139,11 @@ class Pokestop extends Model { const ts = Math.floor(Date.now() / 1000) const { queryLimits, stopValidDataLimit, hideOldPokestops } = config.getSafe('api') - const manualIds = parseManualIds(args.filters.onlyManualId) + const manualId = + typeof args.filters.onlyManualId === 'string' || + typeof args.filters.onlyManualId === 'number' + ? args.filters.onlyManualId + : null const { lures: lurePerms, @@ -187,7 +191,7 @@ class Pokestop extends Model { } Pokestop.joinIncident(query, hasMultiInvasions, isMad, multiInvasionMs) applyManualIdFilter(query, { - manualIds, + manualId, latColumn: isMad ? 'latitude' : 'lat', lonColumn: isMad ? 'longitude' : 'lon', idColumn: isMad ? 'pokestop.pokestop_id' : 'id', diff --git a/server/src/models/Portal.js b/server/src/models/Portal.js index 9ece40081..91ffc08b3 100644 --- a/server/src/models/Portal.js +++ b/server/src/models/Portal.js @@ -3,7 +3,7 @@ const { Model } = require('objection') const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') -const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') +const { applyManualIdFilter } = require('../utils/manualFilter') class Portal extends Model { static get tableName() { @@ -29,10 +29,14 @@ class Portal extends Model { maxLat, maxLon, } = args - const manualIds = parseManualIds(args.filters.onlyManualId) + const manualId = + typeof args.filters.onlyManualId === 'string' || + typeof args.filters.onlyManualId === 'number' + ? args.filters.onlyManualId + : null const query = this.query() applyManualIdFilter(query, { - manualIds, + manualId, latColumn: 'lat', lonColumn: 'lon', idColumn: 'id', diff --git a/server/src/models/Route.js b/server/src/models/Route.js index dac1876eb..179c83699 100644 --- a/server/src/models/Route.js +++ b/server/src/models/Route.js @@ -4,7 +4,7 @@ const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') const { getEpoch } = require('../utils/getClientTime') -const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') +const { applyManualIdFilter } = require('../utils/manualFilter') const GET_ALL_SELECT = /** @type {const} */ ([ 'id', @@ -45,7 +45,11 @@ class Route extends Model { const ts = getEpoch() - config.getSafe('api.routeUpdateLimit') * 24 * 60 * 60 const distanceInMeters = (onlyDistance || [0.5, 100]).map((x) => x * 1000) - const manualIds = parseManualIds(args.filters.onlyManualId) + const manualId = + typeof args.filters.onlyManualId === 'string' || + typeof args.filters.onlyManualId === 'number' + ? args.filters.onlyManualId + : null const startLatitude = isMad ? 'start_poi_latitude' : 'start_lat' const startLongitude = isMad ? 'start_poi_longitude' : 'start_lon' @@ -58,7 +62,7 @@ class Route extends Model { isMad ? GET_MAD_ALL_SELECT : GET_ALL_SELECT, ) applyManualIdFilter(query, { - manualIds, + manualId, latColumn: startLatitude, lonColumn: startLongitude, idColumn, @@ -84,7 +88,7 @@ class Route extends Model { .union((qb) => { qb.select(isMad ? GET_MAD_ALL_SELECT : GET_ALL_SELECT) applyManualIdFilter(qb, { - manualIds, + manualId, latColumn: endLatitude, lonColumn: endLongitude, idColumn, diff --git a/server/src/models/Station.js b/server/src/models/Station.js index 2306bc413..78006239b 100644 --- a/server/src/models/Station.js +++ b/server/src/models/Station.js @@ -4,7 +4,7 @@ const config = require('@rm/config') const i18next = require('i18next') const { getAreaSql } = require('../utils/getAreaSql') -const { applyManualIdFilter, parseManualIds } = require('../utils/manualFilter') +const { applyManualIdFilter } = require('../utils/manualFilter') const { getEpoch } = require('../utils/getClientTime') const { state } = require('../services/state') @@ -31,7 +31,11 @@ class Station extends Model { onlyGmaxStationed, } = args.filters const ts = getEpoch() - const manualIds = parseManualIds(args.filters.onlyManualId) + const manualId = + typeof args.filters.onlyManualId === 'string' || + typeof args.filters.onlyManualId === 'number' + ? args.filters.onlyManualId + : null const select = [ 'id', @@ -45,7 +49,7 @@ class Station extends Model { const query = this.query() applyManualIdFilter(query, { - manualIds, + manualId, latColumn: 'lat', lonColumn: 'lon', idColumn: 'id', diff --git a/server/src/utils/manualFilter.js b/server/src/utils/manualFilter.js index 5fff702da..349adcc65 100644 --- a/server/src/utils/manualFilter.js +++ b/server/src/utils/manualFilter.js @@ -1,40 +1,47 @@ // @ts-check -/** - * @param {unknown} value - * @returns {(string | number)[]} - */ -function parseManualIds(value) { - if (Array.isArray(value)) { - return value.filter((id) => id !== undefined && id !== null && id !== '') - } - return value !== undefined && value !== null && value !== '' ? [value] : [] -} - /** * @param {import('objection').QueryBuilder} query * @param {{ - * manualIds: (string | number)[], + * manualId?: string | number | null, * latColumn: string, * lonColumn: string, * idColumn: string, * bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number }, * }} options + * @returns {string | number | null} */ function applyManualIdFilter(query, options) { - const { manualIds, latColumn, lonColumn, idColumn, bounds } = options - if (manualIds.length) { + const { + manualId: rawManual, + latColumn, + lonColumn, + idColumn, + bounds, + } = options + + const manualId = + rawManual !== undefined && + rawManual !== null && + rawManual !== '' && + (typeof rawManual === 'string' || typeof rawManual === 'number') + ? rawManual + : null + + if (manualId !== null) { query.where((builder) => { builder .whereBetween(latColumn, [bounds.minLat, bounds.maxLat]) .andWhereBetween(lonColumn, [bounds.minLon, bounds.maxLon]) - .orWhereIn(idColumn, manualIds) + .orWhere(idColumn, manualId) }) } else { query .whereBetween(latColumn, [bounds.minLat, bounds.maxLat]) .andWhereBetween(lonColumn, [bounds.minLon, bounds.maxLon]) } + + return manualId } -module.exports = { parseManualIds, applyManualIdFilter } +module.exports = { applyManualIdFilter } From f14f63dc67d20a4cb82d95a7d79b9f2f64ef572a Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 24 Sep 2025 19:23:00 -0700 Subject: [PATCH 7/8] chore: code style --- server/src/models/Gym.js | 6 +----- server/src/models/Nest.js | 7 +------ server/src/models/Pokemon.js | 27 ++++++++++----------------- server/src/models/Pokestop.js | 8 +------- server/src/models/Portal.js | 7 +------ server/src/models/Route.js | 10 ++-------- server/src/models/Station.js | 8 +------- server/src/utils/manualFilter.js | 26 ++++++++++++++++++-------- 8 files changed, 35 insertions(+), 64 deletions(-) diff --git a/server/src/models/Gym.js b/server/src/models/Gym.js index 9761e7011..e26fafd75 100644 --- a/server/src/models/Gym.js +++ b/server/src/models/Gym.js @@ -136,13 +136,9 @@ class Gym extends Model { const latCol = isMad ? 'latitude' : 'lat' const lonCol = isMad ? 'longitude' : 'lon' const idCol = isMad ? 'gym.gym_id' : 'id' - const manualId = - typeof onlyManualId === 'string' || typeof onlyManualId === 'number' - ? onlyManualId - : null applyManualIdFilter(query, { - manualId, + manualId: onlyManualId, latColumn: latCol, lonColumn: lonCol, idColumn: idCol, diff --git a/server/src/models/Nest.js b/server/src/models/Nest.js index 144bf839b..7bb9af8f2 100644 --- a/server/src/models/Nest.js +++ b/server/src/models/Nest.js @@ -28,14 +28,9 @@ class Nest extends Model { static async getAll(perms, args, { polygon }) { const { areaRestrictions } = perms const { minLat, minLon, maxLat, maxLon, filters } = args - const manualId = - typeof filters.onlyManualId === 'string' || - typeof filters.onlyManualId === 'number' - ? filters.onlyManualId - : null const query = this.query().select(['*', 'nest_id AS id']) applyManualIdFilter(query, { - manualId, + manualId: filters.onlyManualId, latColumn: 'lat', lonColumn: 'lon', idColumn: 'nest_id', diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index b620b155d..62a296a17 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -14,7 +14,10 @@ const config = require('@rm/config') const { getAreaSql } = require('../utils/getAreaSql') const { filterRTree } = require('../utils/filterRTree') const { fetchJson } = require('../utils/fetchJson') -const { applyManualIdFilter } = require('../utils/manualFilter') +const { + applyManualIdFilter, + normalizeManualId, +} = require('../utils/manualFilter') const { IV_CALC, LEVEL_CALC, @@ -147,15 +150,11 @@ class Pokemon extends Model { if (!noPokemonSelect) return [] } - const manualIdRaw = - typeof args.filters.onlyManualId === 'string' || - typeof args.filters.onlyManualId === 'number' - ? args.filters.onlyManualId - : null - const query = this.query() - let manualId = null + const manualIdFilter = normalizeManualId(args.filters.onlyManualId) + + let manualId = manualIdFilter const pokemonIds = [] const pokemonForms = [] @@ -184,7 +183,7 @@ class Pokemon extends Model { isMad ? this.knex().fn.now() : ts, ) manualId = applyManualIdFilter(query, { - manualId: manualIdRaw, + manualId: manualIdFilter, latColumn: isMad ? 'pokemon.latitude' : 'lat', lonColumn: isMad ? 'pokemon.longitude' : 'lon', idColumn: isMad ? 'pokemon.encounter_id' : 'id', @@ -256,8 +255,6 @@ class Pokemon extends Model { if (!getAreaSql(query, areaRestrictions, onlyAreas, isMad, 'pokemon')) { return [] } - } else { - manualId = manualIdRaw } const filters = mem @@ -687,11 +684,7 @@ class Pokemon extends Model { const { isMad, hasSize, hasHeight, mem, secret, httpAuth } = ctx const ts = Math.floor(Date.now() / 1000) const { filterMap, globalFilter } = this.getFilters(perms, args, ctx) - const manualIdRaw = - typeof args.filters.onlyManualId === 'string' || - typeof args.filters.onlyManualId === 'number' - ? args.filters.onlyManualId - : null + const manualIdFilter = normalizeManualId(args.filters.onlyManualId) const queryLimits = config.getSafe('api.queryLimits') if (!perms.iv && !perms.pvp) { @@ -732,7 +725,7 @@ class Pokemon extends Model { return [] } const manualId = applyManualIdFilter(query, { - manualId: manualIdRaw, + manualId: manualIdFilter, latColumn: isMad ? 'pokemon.latitude' : 'lat', lonColumn: isMad ? 'pokemon.longitude' : 'lon', idColumn: isMad ? 'pokemon.encounter_id' : 'id', diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index 18b874dfc..0ffff1032 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -139,12 +139,6 @@ class Pokestop extends Model { const ts = Math.floor(Date.now() / 1000) const { queryLimits, stopValidDataLimit, hideOldPokestops } = config.getSafe('api') - const manualId = - typeof args.filters.onlyManualId === 'string' || - typeof args.filters.onlyManualId === 'number' - ? args.filters.onlyManualId - : null - const { lures: lurePerms, quests: questPerms, @@ -191,7 +185,7 @@ class Pokestop extends Model { } Pokestop.joinIncident(query, hasMultiInvasions, isMad, multiInvasionMs) applyManualIdFilter(query, { - manualId, + manualId: args.filters.onlyManualId, latColumn: isMad ? 'latitude' : 'lat', lonColumn: isMad ? 'longitude' : 'lon', idColumn: isMad ? 'pokestop.pokestop_id' : 'id', diff --git a/server/src/models/Portal.js b/server/src/models/Portal.js index 91ffc08b3..75beef5df 100644 --- a/server/src/models/Portal.js +++ b/server/src/models/Portal.js @@ -29,14 +29,9 @@ class Portal extends Model { maxLat, maxLon, } = args - const manualId = - typeof args.filters.onlyManualId === 'string' || - typeof args.filters.onlyManualId === 'number' - ? args.filters.onlyManualId - : null const query = this.query() applyManualIdFilter(query, { - manualId, + manualId: args.filters.onlyManualId, latColumn: 'lat', lonColumn: 'lon', idColumn: 'id', diff --git a/server/src/models/Route.js b/server/src/models/Route.js index 179c83699..a78c1e556 100644 --- a/server/src/models/Route.js +++ b/server/src/models/Route.js @@ -45,12 +45,6 @@ class Route extends Model { const ts = getEpoch() - config.getSafe('api.routeUpdateLimit') * 24 * 60 * 60 const distanceInMeters = (onlyDistance || [0.5, 100]).map((x) => x * 1000) - const manualId = - typeof args.filters.onlyManualId === 'string' || - typeof args.filters.onlyManualId === 'number' - ? args.filters.onlyManualId - : null - const startLatitude = isMad ? 'start_poi_latitude' : 'start_lat' const startLongitude = isMad ? 'start_poi_longitude' : 'start_lon' const distanceMeters = isMad ? 'route_distance_meters' : 'distance_meters' @@ -61,8 +55,8 @@ class Route extends Model { const query = this.query().select( isMad ? GET_MAD_ALL_SELECT : GET_ALL_SELECT, ) - applyManualIdFilter(query, { - manualId, + const manualId = applyManualIdFilter(query, { + manualId: args.filters.onlyManualId, latColumn: startLatitude, lonColumn: startLongitude, idColumn, diff --git a/server/src/models/Station.js b/server/src/models/Station.js index 78006239b..7f3078f60 100644 --- a/server/src/models/Station.js +++ b/server/src/models/Station.js @@ -31,12 +31,6 @@ class Station extends Model { onlyGmaxStationed, } = args.filters const ts = getEpoch() - const manualId = - typeof args.filters.onlyManualId === 'string' || - typeof args.filters.onlyManualId === 'number' - ? args.filters.onlyManualId - : null - const select = [ 'id', 'name', @@ -49,7 +43,7 @@ class Station extends Model { const query = this.query() applyManualIdFilter(query, { - manualId, + manualId: args.filters.onlyManualId, latColumn: 'lat', lonColumn: 'lon', idColumn: 'id', diff --git a/server/src/utils/manualFilter.js b/server/src/utils/manualFilter.js index 349adcc65..120b4807a 100644 --- a/server/src/utils/manualFilter.js +++ b/server/src/utils/manualFilter.js @@ -1,5 +1,21 @@ // @ts-check +/** + * @param {unknown} manualId + * @returns {string | number | null} + */ +function normalizeManualId(manualId) { + if ( + manualId === undefined || + manualId === null || + manualId === '' || + (typeof manualId !== 'string' && typeof manualId !== 'number') + ) { + return null + } + return manualId +} + /** * @param {import('objection').QueryBuilder} query * @param {{ @@ -20,13 +36,7 @@ function applyManualIdFilter(query, options) { bounds, } = options - const manualId = - rawManual !== undefined && - rawManual !== null && - rawManual !== '' && - (typeof rawManual === 'string' || typeof rawManual === 'number') - ? rawManual - : null + const manualId = normalizeManualId(rawManual) if (manualId !== null) { query.where((builder) => { @@ -44,4 +54,4 @@ function applyManualIdFilter(query, options) { return manualId } -module.exports = { applyManualIdFilter } +module.exports = { applyManualIdFilter, normalizeManualId } From 5b684dcfab6f90a45e23c403a523cda14720248d Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 24 Sep 2025 19:39:26 -0700 Subject: [PATCH 8/8] fix: ai suggested idk i never used this --- src/features/gym/GymTile.jsx | 1 + src/services/desktopNotification.js | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/features/gym/GymTile.jsx b/src/features/gym/GymTile.jsx index 61c5333ef..cdf06e129 100644 --- a/src/features/gym/GymTile.jsx +++ b/src/features/gym/GymTile.jsx @@ -159,6 +159,7 @@ const BaseGymTile = (gym) => { const handlePopupOpen = useManualPopupTracker('gyms', gym.id) if (hasRaid) { sendNotification(`${gym.id}-${hasHatched}`, gym.name, 'raids', { + manualId: gym.id, lat: gym.lat, lon: gym.lon, expire: timerToDisplay, diff --git a/src/services/desktopNotification.js b/src/services/desktopNotification.js index f11203891..31e0d952f 100644 --- a/src/services/desktopNotification.js +++ b/src/services/desktopNotification.js @@ -42,7 +42,7 @@ export function sendNotification(key, title, category, options) { ) if (userSettings.enabled && userSettings[category]) { if (getPermission() === 'granted') { - const { lat, lon, audio, expire, ...rest } = options + const { lat, lon, audio, expire, manualId, ...rest } = options || {} const countdown = expire ? expire * 1000 - Date.now() : 1 if (countdown < 0) return cache.set(key, countdown) @@ -65,7 +65,12 @@ export function sendNotification(key, title, category, options) { } if (lat && lon) { const { map } = useMapStore.getState() - useMemory.setState({ manualParams: { category, id: key } }) + useMemory.setState({ + manualParams: { + category, + id: manualId ?? key, + }, + }) map.flyTo([lat, lon], 16) } notif.close()