From 172129ea4e1c073b07868a7692e68e00874963e5 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:20:26 -0500 Subject: [PATCH 01/15] refactor: search results --- server/src/configs/default.json | 2 + server/src/models/Gym.js | 14 +++- server/src/models/Nest.js | 5 +- server/src/models/Pokemon.js | 22 +++++- server/src/models/Pokestop.js | 24 +++++-- server/src/models/Portal.js | 5 +- server/src/services/DbCheck.js | 68 +++++++++++++++---- server/src/services/functions/getBbox.js | 31 +++++++++ .../functions/validateSelectedWebhook.js | 2 +- 9 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 server/src/services/functions/getBbox.js diff --git a/server/src/configs/default.json b/server/src/configs/default.json index 292ea6f7d..455c1e793 100644 --- a/server/src/configs/default.json +++ b/server/src/configs/default.json @@ -98,6 +98,8 @@ "portalUpdateLimit": 30, "weatherCellLimit": 3, "searchResultsLimit": 15, + "searchSoftKmLimit": 10, + "searchHardKmLimit": 100, "nestHemisphere": "north", "gymValidDataLimit": 30, "hideOldGyms": false, diff --git a/server/src/models/Gym.js b/server/src/models/Gym.js index 41dd9c9e0..69f394f25 100644 --- a/server/src/models/Gym.js +++ b/server/src/models/Gym.js @@ -459,7 +459,7 @@ class Gym extends Model { } } - static async search(perms, args, { isMad }, distance) { + static async search(perms, args, { isMad }, distance, bbox) { const { areaRestrictions } = perms const { onlyAreas = [], search } = args const query = this.query() @@ -471,6 +471,8 @@ class Gym extends Model { 'url', distance, ]) + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) .whereRaw(`LOWER(name) LIKE '%${search}%'`) .limit(searchResultsLimit) .orderBy('distance') @@ -485,7 +487,13 @@ class Gym extends Model { return query } - static async searchRaids(perms, args, { isMad, hasAlignment }, distance) { + static async searchRaids( + perms, + args, + { isMad, hasAlignment }, + distance, + bbox, + ) { const { search, locale, onlyAreas = [] } = args const pokemonIds = Object.keys(Event.masterfile.pokemon).filter((pkmn) => i18next.t(`poke_${pkmn}`, { lng: locale }).toLowerCase().includes(search), @@ -507,6 +515,8 @@ class Gym extends Model { : 'raid_pokemon_evolution', distance, ]) + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) .whereIn(isMad ? 'pokemon_id' : 'raid_pokemon_id', pokemonIds) .limit(searchResultsLimit) .orderBy('distance') diff --git a/server/src/models/Nest.js b/server/src/models/Nest.js index 7350a8912..24a03d983 100644 --- a/server/src/models/Nest.js +++ b/server/src/models/Nest.js @@ -139,9 +139,10 @@ class Nest extends Model { * @param {object} args * @param {import("@rm/types").DbContext} ctx * @param {import('objection').Raw} distance + * @param {ReturnType} bbox * @returns {Promise} */ - static async search(perms, args, { isMad }, distance) { + static async search(perms, args, { isMad }, distance, bbox) { const { search, locale, onlyAreas = [] } = args const pokemonIds = Object.keys(Event.masterfile.pokemon).filter((pkmn) => i18next.t(`poke_${pkmn}`, { lng: locale }).toLowerCase().includes(search), @@ -159,6 +160,8 @@ class Nest extends Model { 'pokemon_form AS nest_pokemon_form', distance, ]) + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) .whereNotNull('pokemon_id') .where((builder) => { builder diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index bd86925af..8387c78be 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -567,9 +567,18 @@ class Pokemon extends Model { * @param {object} args * @param {import("@rm/types").DbContext} ctx * @param {number} distance + * @param {ReturnType} bbox + * @param {number} [maxDistance] * @returns {Promise[]>} */ - static async search(perms, args, { isMad, mem, secret }, distance) { + static async search( + perms, + args, + { isMad, mem, secret }, + distance, + bbox, + maxDistance, + ) { const { search, locale, onlyAreas = [] } = args const pokemonIds = Object.keys(Event.masterfile.pokemon).filter((pkmn) => i18next.t(`poke_${pkmn}`, { lng: locale }).toLowerCase().includes(search), @@ -578,6 +587,8 @@ class Pokemon extends Model { const query = this.query() .select(['pokemon_id', distance]) .whereIn('pokemon_id', pokemonIds) + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) .andWhere( isMad ? 'disappear_time' : 'expire_timestamp', '>=', @@ -618,6 +629,15 @@ class Pokemon extends Model { latitude: args.lat, longitude: args.lon, }, + min: { + latitude: bbox.minLat, + longitude: bbox.minLon, + }, + max: { + latitude: bbox.maxLat, + longitude: bbox.maxLon, + }, + maxDistance, limit: searchResultsLimit * 4, searchIds: pokemonIds.map((id) => +id), global: {}, diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index 88c4a9649..6b6966c18 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -1599,7 +1599,7 @@ class Pokestop extends Model { return quest } - static async search(perms, args, { isMad }, distance) { + static async search(perms, args, { isMad }, distance, bbox) { const { onlyAreas = [], search } = args const query = this.query() .select([ @@ -1610,6 +1610,8 @@ class Pokestop extends Model { isMad ? 'image AS url' : 'url', distance, ]) + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) .whereRaw(`LOWER(name) LIKE '%${search}%'`) .limit(searchResultsLimit) .orderBy('distance') @@ -1619,10 +1621,15 @@ class Pokestop extends Model { return query } - static async searchQuests(perms, args, { isMad, hasAltQuests }, distance) { + static async searchQuests( + perms, + args, + { isMad, hasAltQuests }, + distance, + bbox, + ) { const { search, onlyAreas = [], locale, lat, lon } = args const midnight = getUserMidnight({ lat, lon }) - const pokemonIds = Object.keys(Event.masterfile.pokemon).filter((pkmn) => i18next .t(`poke_${pkmn}`, { lng: locale }) @@ -1666,6 +1673,8 @@ class Pokestop extends Model { 'quest_title', 'quest_target', ]) + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) .andWhere('quest_timestamp', '>=', midnight || 0) .andWhere((quests) => { if (pokemonIds.length === 1) { @@ -1714,6 +1723,8 @@ class Pokestop extends Model { 'alternative_quest_target', distance, ]) + .whereBetween('lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween('lon', [bbox.minLon, bbox.maxLon]) .andWhere('alternative_quest_timestamp', '>=', midnight || 0) .andWhere((quests) => { if (pokemonIds.length === 1) { @@ -1743,6 +1754,7 @@ class Pokestop extends Model { } const [withAr, withoutAr] = await Promise.all(queries) + const mapped = withAr.map((q) => ({ ...q, with_ar: q.with_ar ?? true })) if (withoutAr) { const remapped = withoutAr.map((result) => ({ @@ -1759,7 +1771,7 @@ class Pokestop extends Model { } mapped.sort((a, b) => a.distance - b.distance) - mapped.length = searchResultsLimit + if (mapped.length > searchResultsLimit) mapped.length = searchResultsLimit return mapped .map((result) => @@ -1768,7 +1780,7 @@ class Pokestop extends Model { .filter(Boolean) } - static async searchLures(perms, args, { isMad }, distance) { + static async searchLures(perms, args, { isMad }, distance, bbox) { const { search, onlyAreas = [], locale } = args const ts = Math.floor(Date.now() / 1000) @@ -1794,6 +1806,8 @@ class Pokestop extends Model { : 'lure_expire_timestamp', distance, ]) + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) .andWhere( isMad ? 'lure_expiration' : 'lure_expire_timestamp', '>=', diff --git a/server/src/models/Portal.js b/server/src/models/Portal.js index 2bfae442b..b3e9813de 100644 --- a/server/src/models/Portal.js +++ b/server/src/models/Portal.js @@ -49,14 +49,17 @@ class Portal extends Model { * @param {object} args * @param {import("@rm/types").DbContext} context * @param {ReturnType} distance + * @param {ReturnType} bbox * @returns {Promise} */ - static async search(perms, args, { isMad }, distance) { + static async search(perms, args, { isMad }, distance, bbox) { const { areaRestrictions } = perms const { onlyAreas = [], search } = args const query = this.query() .select(['name', 'id', 'lat', 'lon', 'url', distance]) .whereRaw(`LOWER(name) LIKE '%${search}%'`) + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) .andWhere( 'updated', '>', diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index d9b54e3e3..64bb110a2 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -1,9 +1,11 @@ +/* eslint-disable no-await-in-loop */ const { knex } = require('knex') const { raw } = require('objection') const extend = require('extend') const config = require('@rm/config') const { log, HELPERS } = require('@rm/logger') +const { getBboxFromCenter } = require('./functions/getBbox') /** * @type {import("@rm/types").DbCheckClass} @@ -433,20 +435,60 @@ module.exports = class DbCheck { * @param {'search' | string} method * @returns {Promise} */ - async search(model, perms, args, method = 'search') { - const data = await Promise.all( - this.models[model].map(async ({ SubModel, ...source }) => - SubModel[method]( - perms, - args, - source, - this.getDistance(args, source.isMad), + async search( + model, + perms, + args, + method = 'search', + bboxDistance = config.getSafe('api.searchSoftKmLimit'), + ) { + let deDuped = [] + let count = 0 + while (deDuped.length < this.searchLimit) { + count += 1 + const bbox = getBboxFromCenter(args.lat, args.lon, bboxDistance) + const distance = bboxDistance + const data = await Promise.all( + this.models[model].map(async ({ SubModel, ...source }) => + SubModel[method]( + perms, + args, + source, + this.getDistance(args, source.isMad), + bbox, + distance, + ), ), - ), - ) - const deDuped = DbCheck.deDupeResults(data).sort( - (a, b) => a.distance - b.distance, - ) + ) + deDuped = DbCheck.deDupeResults(data) + log.debug( + HELPERS.db, + 'Attempt #', + count, + 'and received', + deDuped.length, + 'at distance', + bboxDistance, + ) + if ( + deDuped.length >= this.searchLimit || + bboxDistance >= config.getSafe('api.searchHardKmLimit') + ) { + break + } + bboxDistance += config.getSafe('api.searchSoftKmLimit') + } + if (count > 1) { + log.info( + HELPERS.db, + 'Searched', + count, + 'time to get', + deDuped.length, + `results for ${method} on model ${model}`, + ) + } + deDuped.sort((a, b) => a.distance - b.distance) if (deDuped.length > this.searchLimit) { deDuped.length = this.searchLimit } diff --git a/server/src/services/functions/getBbox.js b/server/src/services/functions/getBbox.js new file mode 100644 index 000000000..9a1d5bfac --- /dev/null +++ b/server/src/services/functions/getBbox.js @@ -0,0 +1,31 @@ +// @ts-check +const { default: destination } = require('@turf/destination') +const { point } = require('@turf/helpers') + +/** + * + * @param {number} centerLat + * @param {number} centerLon + * @param {number} distanceKm + * @returns + */ +function getBboxFromCenter(centerLat, centerLon, distanceKm) { + const center = point([centerLon, centerLat]) + const [minLon, minLat] = destination(center, distanceKm, 225, { + units: 'kilometers', + }).geometry.coordinates + const [maxLon, maxLat] = destination(center, distanceKm, 45, { + units: 'kilometers', + }).geometry.coordinates + + return { + minLat, + minLon, + maxLat, + maxLon, + } +} + +module.exports = { + getBboxFromCenter, +} diff --git a/server/src/services/functions/validateSelectedWebhook.js b/server/src/services/functions/validateSelectedWebhook.js index 209ec81ba..ac39ee4e5 100644 --- a/server/src/services/functions/validateSelectedWebhook.js +++ b/server/src/services/functions/validateSelectedWebhook.js @@ -1,7 +1,7 @@ // @ts-check /** * - * @param {import("@rm/types").User} user + * @param {import("@rm/types").ExpressUser} user * @param {import("@rm/types").GqlContext['Db']} Db * @param {import("@rm/types").GqlContext['Event']} Event */ From d062e5219830039e1ae381e879c890293dd67f3f Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:49:25 -0500 Subject: [PATCH 02/15] fix: slightly more aggressive additions when there's 0 --- server/src/services/DbCheck.js | 38 ++++++++++++++++------------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index 64bb110a2..d74003e45 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -7,6 +7,9 @@ const config = require('@rm/config') const { log, HELPERS } = require('@rm/logger') const { getBboxFromCenter } = require('./functions/getBbox') +const softLimit = config.getSafe('api.searchSoftKmLimit') +const hardLimit = config.getSafe('api.searchHardKmLimit') + /** * @type {import("@rm/types").DbCheckClass} */ @@ -435,19 +438,14 @@ module.exports = class DbCheck { * @param {'search' | string} method * @returns {Promise} */ - async search( - model, - perms, - args, - method = 'search', - bboxDistance = config.getSafe('api.searchSoftKmLimit'), - ) { + async search(model, perms, args, method = 'search') { let deDuped = [] let count = 0 + let distance = softLimit while (deDuped.length < this.searchLimit) { count += 1 - const bbox = getBboxFromCenter(args.lat, args.lon, bboxDistance) - const distance = bboxDistance + const bbox = getBboxFromCenter(args.lat, args.lon, distance) + const local = distance const data = await Promise.all( this.models[model].map(async ({ SubModel, ...source }) => SubModel[method]( @@ -456,34 +454,34 @@ module.exports = class DbCheck { source, this.getDistance(args, source.isMad), bbox, - distance, + local, ), ), ) deDuped = DbCheck.deDupeResults(data) log.debug( HELPERS.db, - 'Attempt #', + 'Search attempt #', count, - 'and received', + '| received:', deDuped.length, - 'at distance', - bboxDistance, + '| distance:', + distance, ) - if ( - deDuped.length >= this.searchLimit || - bboxDistance >= config.getSafe('api.searchHardKmLimit') - ) { + if (deDuped.length >= this.searchLimit || distance >= hardLimit) { break } - bboxDistance += config.getSafe('api.searchSoftKmLimit') + distance += softLimit + if (deDuped.length === 0) { + distance = Math.min(distance + softLimit, hardLimit) + } } if (count > 1) { log.info( HELPERS.db, 'Searched', count, - 'time to get', + '| received:', deDuped.length, `results for ${method} on model ${model}`, ) From 15a080cf5c05a9bb4b07c1d614658f48f6067e81 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:28:05 -0500 Subject: [PATCH 03/15] refactor: some limit adjustments --- server/src/models/Pokemon.js | 6 ++---- server/src/services/DbCheck.js | 4 ++-- src/components/layout/dialogs/Search.jsx | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index 8387c78be..b5bb15e8c 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -638,7 +638,7 @@ class Pokemon extends Model { longitude: bbox.maxLon, }, maxDistance, - limit: searchResultsLimit * 4, + limit: searchResultsLimit, searchIds: pokemonIds.map((id) => +id), global: {}, filters: {}, @@ -649,9 +649,7 @@ class Pokemon extends Model { ) return results .filter( - (item, i) => - i < searchResultsLimit && - (!mem || filterRTree(item, perms.areaRestrictions, onlyAreas)), + (item) => !mem || filterRTree(item, perms.areaRestrictions, onlyAreas), ) .map((poke) => ({ ...poke, diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index d74003e45..79bb61aa2 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -472,8 +472,8 @@ module.exports = class DbCheck { break } distance += softLimit - if (deDuped.length === 0) { - distance = Math.min(distance + softLimit, hardLimit) + if (deDuped.length === 0 && distance < hardLimit) { + distance += softLimit } } if (count > 1) { diff --git a/src/components/layout/dialogs/Search.jsx b/src/components/layout/dialogs/Search.jsx index 5726a6a59..06f6f5802 100644 --- a/src/components/layout/dialogs/Search.jsx +++ b/src/components/layout/dialogs/Search.jsx @@ -290,7 +290,7 @@ export default function Search() {
({ ...option, i }))} From 6e6bbffc162df7bbc54c9c98b546bfe3698d25a0 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Wed, 17 Jan 2024 22:37:30 -0500 Subject: [PATCH 04/15] fix: more limit adjustments --- server/src/services/DbCheck.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index 79bb61aa2..b96b4b534 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -471,10 +471,14 @@ module.exports = class DbCheck { if (deDuped.length >= this.searchLimit || distance >= hardLimit) { break } - distance += softLimit - if (deDuped.length === 0 && distance < hardLimit) { + if (deDuped.length === 0) { + distance += softLimit * 3 + } else if (deDuped.length < this.searchLimit / 4) { + distance += softLimit * 2 + } else { distance += softLimit } + distance = Math.min(distance, hardLimit) } if (count > 1) { log.info( From 3acfef4bf14ddcaec7163dde943abdcf239f7371 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Wed, 17 Jan 2024 23:11:44 -0500 Subject: [PATCH 05/15] fix: add time log --- server/src/services/DbCheck.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index b96b4b534..c7d9adc44 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -442,6 +442,7 @@ module.exports = class DbCheck { let deDuped = [] let count = 0 let distance = softLimit + const startTime = Date.now() while (deDuped.length < this.searchLimit) { count += 1 const bbox = getBboxFromCenter(args.lat, args.lon, distance) @@ -468,12 +469,16 @@ module.exports = class DbCheck { '| distance:', distance, ) - if (deDuped.length >= this.searchLimit || distance >= hardLimit) { + if ( + deDuped.length >= this.searchLimit || + distance >= hardLimit || + Date.now() - startTime > 3_000 + ) { break } if (deDuped.length === 0) { - distance += softLimit * 3 - } else if (deDuped.length < this.searchLimit / 4) { + distance += softLimit * 4 + } else if (deDuped.length < this.searchLimit / 2) { distance += softLimit * 2 } else { distance += softLimit @@ -488,6 +493,8 @@ module.exports = class DbCheck { '| received:', deDuped.length, `results for ${method} on model ${model}`, + '| time:', + +((Date.now() - startTime) / 1000).toFixed(2), ) } deDuped.sort((a, b) => a.distance - b.distance) From 8f117412c5f4e9ef5588332472d21d6f20169e76 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Wed, 17 Jan 2024 23:12:58 -0500 Subject: [PATCH 06/15] fix: searchable --- server/src/graphql/resolvers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index 9eee7c54c..2b2ada550 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -465,7 +465,7 @@ const resolvers = { }, searchable: (_, __, { perms }) => { const options = config.getSafe('api.searchable') - return Object.keys(options).filter((k) => options[k] && perms[k]) + return Object.keys(options).filter((k) => perms[k] && options[k]) }, spawnpoints: (_, args, { perms, Db }) => { if (perms?.spawnpoints) { From 39ac7690806dbedf5424515870beda64c0cb229a Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Wed, 17 Jan 2024 23:16:00 -0500 Subject: [PATCH 07/15] fix: don't always have to hit the limit --- server/src/services/DbCheck.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index c7d9adc44..57fb85bd8 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -470,7 +470,7 @@ module.exports = class DbCheck { distance, ) if ( - deDuped.length >= this.searchLimit || + deDuped.length >= this.searchLimit * 0.75 || distance >= hardLimit || Date.now() - startTime > 3_000 ) { From 8fa91e1691b8c52f83e7a9ce39aaafab5a5822a8 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:00:17 -0500 Subject: [PATCH 08/15] fix: more pokemon search refinements --- server/src/services/DbCheck.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index 57fb85bd8..0609efa6d 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -442,8 +442,10 @@ module.exports = class DbCheck { let deDuped = [] let count = 0 let distance = softLimit + const max = model === 'Pokemon' ? hardLimit / 2 : hardLimit const startTime = Date.now() while (deDuped.length < this.searchLimit) { + const loopTime = Date.now() count += 1 const bbox = getBboxFromCenter(args.lat, args.lon, distance) const local = distance @@ -468,22 +470,24 @@ module.exports = class DbCheck { deDuped.length, '| distance:', distance, + '| time:', + +((Date.now() - loopTime) / 1000).toFixed(2), ) if ( - deDuped.length >= this.searchLimit * 0.75 || - distance >= hardLimit || - Date.now() - startTime > 3_000 + deDuped.length >= this.searchLimit * 0.5 || + distance >= max || + Date.now() - startTime > 2_000 ) { break } if (deDuped.length === 0) { distance += softLimit * 4 - } else if (deDuped.length < this.searchLimit / 2) { + } else if (deDuped.length < this.searchLimit / 4) { distance += softLimit * 2 } else { distance += softLimit } - distance = Math.min(distance, hardLimit) + distance = Math.min(distance, max) } if (count > 1) { log.info( From ad929ee788162a69bf21d8bf3271a420a58dd09c Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:43:13 -0500 Subject: [PATCH 09/15] refactor: frontend components --- packages/types/lib/scanner.d.ts | 1 + src/components/layout/Nav.jsx | 2 +- src/components/layout/dialogs/Search.jsx | 531 ------------------ .../layout/dialogs/search/OptionImage.jsx | 172 ++++++ .../layout/dialogs/search/index.jsx | 114 ++++ .../layout/dialogs/search/renderInput.jsx | 152 +++++ .../layout/dialogs/search/renderOption.jsx | 98 ++++ .../layout/dialogs/search/useSendSearch.js | 111 ++++ src/components/layout/general/QuestTitle.jsx | 21 +- 9 files changed, 662 insertions(+), 540 deletions(-) delete mode 100644 src/components/layout/dialogs/Search.jsx create mode 100644 src/components/layout/dialogs/search/OptionImage.jsx create mode 100644 src/components/layout/dialogs/search/index.jsx create mode 100644 src/components/layout/dialogs/search/renderInput.jsx create mode 100644 src/components/layout/dialogs/search/renderOption.jsx create mode 100644 src/components/layout/dialogs/search/useSendSearch.js diff --git a/packages/types/lib/scanner.d.ts b/packages/types/lib/scanner.d.ts index c31e495d4..6ccb67af5 100644 --- a/packages/types/lib/scanner.d.ts +++ b/packages/types/lib/scanner.d.ts @@ -237,6 +237,7 @@ export interface Pokemon { pvp_rankings_great_league?: PvpEntry[] pvp_rankings_ultra_league?: PvpEntry[] distance?: number + shiny?: boolean } export type FullPokemon = FullModel diff --git a/src/components/layout/Nav.jsx b/src/components/layout/Nav.jsx index e02f48568..72410283f 100644 --- a/src/components/layout/Nav.jsx +++ b/src/components/layout/Nav.jsx @@ -8,7 +8,7 @@ import FilterMenu from './dialogs/filters/FilterMenu' import UserOptions from './dialogs/UserOptions' import Tutorial from './dialogs/tutorial/Tutorial' import UserProfile from './dialogs/profile' -import Search from './dialogs/Search' +import Search from './dialogs/search' import MessageOfTheDay from './dialogs/Motd' import DonorPage from './dialogs/DonorPage' import Feedback from './dialogs/Feedback' diff --git a/src/components/layout/dialogs/Search.jsx b/src/components/layout/dialogs/Search.jsx deleted file mode 100644 index 06f6f5802..000000000 --- a/src/components/layout/dialogs/Search.jsx +++ /dev/null @@ -1,531 +0,0 @@ -// @ts-check -/* eslint-disable react/destructuring-assignment */ -/* eslint-disable react/no-unstable-nested-components */ -/* eslint-disable no-nested-ternary */ -import * as React from 'react' -import Dialog from '@mui/material/Dialog' -import Divider from '@mui/material/Divider' -import IconButton from '@mui/material/IconButton' -import SearchIcon from '@mui/icons-material/Search' -import Menu from '@mui/material/Menu' -import MenuItem from '@mui/material/MenuItem' -import ListItemIcon from '@mui/material/ListItemIcon' -import ListItemText from '@mui/material/ListItemText' -import TextField from '@mui/material/TextField' -import CircularProgress from '@mui/material/CircularProgress' -import Box from '@mui/material/Box' -import Popper from '@mui/material/Popper' -import Grid from '@mui/material/Grid' -import Typography from '@mui/material/Typography' -import Autocomplete from '@mui/material/Autocomplete' -import { useMap } from 'react-leaflet' -import debounce from 'lodash.debounce' - -import { useTranslation } from 'react-i18next' -import { useLazyQuery, useQuery } from '@apollo/client' - -import NameTT from '@components/popups/common/NameTT' -import { useMemory } from '@hooks/useMemory' -import { useLayoutStore } from '@hooks/useLayoutStore' -import { useStorage } from '@hooks/useStorage' -import Utility from '@services/Utility' -import Query from '@services/Query' -import { SEARCHABLE } from '@services/queries/config' -import getRewardInfo from '@services/functions/getRewardInfo' -import { fromSearchCategory } from '@services/functions/fromSearchCategory' - -import Header from '../general/Header' -import QuestTitle from '../general/QuestTitle' - -function SearchImage({ name }) { - const { t } = useTranslation() - - const darkMode = useStorage((s) => s.darkMode) - const Icons = useMemory((s) => s.Icons) - - return ( - {t(name)} - ) -} - -function EndAdornment({ children }) { - const loading = useMemory((s) => s.searchLoading) - - return ( - <> - - {loading ? ( - ({ - color: theme.palette.getContrastText( - theme.palette.background.default, - ), - })} - /> - ) : ( - - )} - - - {children} - - ) -} - -/** - * - * @param {import('@mui/material').AutocompleteRenderInputParams} props - * @returns - */ -export function FancySearch({ InputProps, ...props }) { - const { t } = useTranslation() - - const searchTab = useStorage((s) => s.searchTab) - - const { data } = useQuery(SEARCHABLE, { - fetchPolicy: 'cache-first', - }) - - const [anchorEl, setAnchorEl] = React.useState(null) - - const handleClose = React.useCallback((selection) => { - if (typeof selection === 'string') { - useStorage.setState({ searchTab: selection }) - } - setAnchorEl(null) - }, []) - - React.useEffect(() => { - if ( - data?.searchable && - (typeof searchTab === 'number' || !data.searchable.includes(searchTab)) - ) { - // searchTab value migration - useStorage.setState({ searchTab: data?.searchable[0] }) - } - }, [searchTab, data]) - - return ( - <> - - setAnchorEl(e.currentTarget)} - > - - - - ), - }} - sx={{ boxShadow: 1 }} - /> - - {(data?.searchable || []).map((option) => ( - handleClose(option)} - > - - - - - - ))} - - - ) -} - -/** @param {string} tab */ -const getBackupName = (tab) => { - switch (tab) { - case 'quests': - case 'pokestops': - return 'unknown_pokestop' - default: - return 'unknown_gym' - } -} - -export default function Search() { - Utility.analytics('/search') - - const { t, i18n } = useTranslation() - const map = useMap() - - const search = useStorage((state) => state.search) - const searchTab = useStorage((state) => state.searchTab) - - const distanceUnit = useMemory((state) => state.config.misc.distanceUnit) - const questMessage = useMemory((state) => state.config.misc.questMessage) - const isMobile = useMemory((s) => s.isMobile) - - const open = useLayoutStore((s) => s.search) - - const [options, setOptions] = React.useState([]) - - const [callSearch, { data, previousData, loading }] = useLazyQuery( - Query.search(searchTab), - ) - - const handleClose = React.useCallback((_, result) => { - useLayoutStore.setState({ search: false }) - if (typeof result === 'object' && 'lat' in result && 'lon' in result) { - map.flyTo([result.lat, result.lon], 16) - useMemory.setState({ - manualParams: { - category: fromSearchCategory(searchTab), - id: result.id, - }, - }) - } - }, []) - - const sendToServer = React.useCallback( - (/** @type {string} */ newSearch) => { - const { lat, lng } = map.getCenter() - const { areas } = useStorage.getState().filters.scanAreas?.filter || { - areas: [], - } - callSearch({ - variables: { - search: newSearch, - category: searchTab, - lat, - lon: lng, - locale: i18n.language, - onlyAreas: areas || [], - }, - }) - }, - [i18n.language, searchTab], - ) - - const debounceChange = React.useMemo( - () => debounce(sendToServer, 250), - [sendToServer], - ) - - const handleChange = React.useCallback( - (e, newValue) => { - if ( - e?.type === 'change' && - (/^[0-9\s\p{L}]+$/u.test(newValue) || newValue === '') - ) { - useStorage.setState({ search: newValue.toLowerCase() }) - debounceChange(newValue.toLowerCase()) - } - }, - [debounceChange], - ) - - React.useEffect(() => { - setOptions( - search - ? (data || previousData)?.[ - searchTab === 'quests' - ? 'searchQuest' - : searchTab === 'lures' - ? 'searchLure' - : 'search' - ] || [] - : [], - ) - Utility.analytics('Global Search', `Search Value: ${search}`, searchTab) - }, [data]) - - React.useEffect(() => { - useMemory.setState({ searchLoading: loading }) - }, [loading]) - - React.useEffect(() => { - // initial search - if (search && open) sendToServer(search) - }, [open]) - - React.useEffect(() => { - if (open) map.closePopup() - }, [open]) - - return ( - - -
- ({ ...option, i }))} - loading={loading} - loadingText={t('searching')} - noOptionsText={t('no_options')} - autoComplete={false} - clearOnBlur={false} - ListboxProps={{ - sx: { maxHeight: '80cqh' }, - }} - fullWidth - clearIcon={null} - popupIcon={null} - sx={{ p: 2 }} - open - PopperComponent={({ children, ...props }) => ( - - {children} - - )} - filterOptions={(o) => o} - onChange={handleClose} - renderInput={FancySearch} - getOptionLabel={(option) => - `${option.id}-${searchTab}-${option.with_ar}` - } - renderOption={(props, option) => ( - ({ - backgroundColor: - option.i % 2 - ? theme.palette.background.default - : theme.palette.grey[ - theme.palette.mode === 'light' ? 100 : 900 - ], - })} - {...props} - > - - - - - - {searchTab === 'pokemon' - ? `${t(`poke_${option.pokemon_id}`)} ${ - option.form && - t(`form_${option.form}`) !== t('poke_type_1') - ? `(${t(`form_${option.form}`)})` - : '' - }${option.iv ? ` - ${option.iv}%` : ''}` - : option.name || t(getBackupName(searchTab))} - -
- {option.quest_title && option.quest_target && ( - - )} - {!!option.lure_expire_timestamp && ( - - {new Date( - option.lure_expire_timestamp * 1000, - ).toLocaleString()} - - )} -
- - - {option.distance} {distanceUnit === 'mi' ? t('mi') : t('km')} - -
- {searchTab === 'quests' && ( - - {questMessage || t(`ar_quest_${Boolean(option.with_ar)}`)} - - )} -
-
- )} - /> - -
- ) -} - -function ResultImage(props) { - const Icons = useMemory((s) => s.Icons) - - if (props.url) { - return ( - { - // @ts-ignore - e.target.onerror = null - if (props.searchTab === 'pokestops') { - // @ts-ignore - e.target.src = - 'https://raw.githubusercontent.com/WatWowMap/wwm-uicons-webp/main/pokestop/0.webp' - } else { - // @ts-ignore - e.target.src = - 'https://raw.githubusercontent.com/WatWowMap/wwm-uicons-webp/main/gym/0.webp' - } - }} - alt={props.url} - height={45} - width={45} - /> - ) - } - if (props.quest_reward_type) { - const { src, amount, tt } = getRewardInfo(props) - - return ( -
- - {typeof { - // @ts-ignore - e.target.onerror = null - // @ts-ignore - e.target.src = - 'https://raw.githubusercontent.com/WatWowMap/wwm-uicons-webp/main/misc/0.webp' - }} - /> - - {!!amount &&
x{amount}
} -
- ) - } - if (props.pokemon_id) { - const { pokemon_id, form, gender, costume, shiny } = props - return ( - - {form} - - ) - } - if (props.raid_pokemon_id) { - const { - raid_pokemon_id, - raid_pokemon_form, - raid_pokemon_gender, - raid_pokemon_costume, - raid_pokemon_evolution, - raid_pokemon_alignment, - } = props - return ( - - {raid_pokemon_id} - - ) - } - if (props.lure_id) { - return ( - - {props.lure_id} - - ) - } - return ( - - {props.nest_pokemon_form} - - ) -} - -const MemoizedResultImage = React.memo( - ResultImage, - (prev, next) => prev.id === next.id, -) diff --git a/src/components/layout/dialogs/search/OptionImage.jsx b/src/components/layout/dialogs/search/OptionImage.jsx new file mode 100644 index 000000000..c98c74349 --- /dev/null +++ b/src/components/layout/dialogs/search/OptionImage.jsx @@ -0,0 +1,172 @@ +// @ts-check +/* eslint-disable react/destructuring-assignment */ +import * as React from 'react' +import Box from '@mui/material/Box' + +import NameTT from '@components/popups/common/NameTT' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' +import getRewardInfo from '@services/functions/getRewardInfo' +import { useTranslateById } from '@hooks/useTranslateById' +import { Img } from '@components/layout/general/Img' + +/** @param {Partial} props */ +function QuestImage(props) { + const { src, amount, tt } = getRewardInfo(props) + const { Icons } = useMemory.getState() + return ( + + + {typeof { + if (e.target instanceof HTMLImageElement) { + e.target.onerror = null + e.target.src = Icons.getRewards(0) + } + }} + /> + + {!!amount &&
x{amount}
} +
+ ) +} + +/** @param {{ url: string }} props */ +function FortImage({ url }) { + const { searchTab } = useStorage.getState() + const { Icons } = useMemory.getState() + return ( + { + if (e.target instanceof HTMLImageElement) { + e.target.onerror = null + e.target.src = + searchTab === 'pokestops' ? Icons.getPokestops(0) : Icons.getGyms(0) + } + }} + alt={url} + height={45} + width={45} + /> + ) +} + +/** @param {Partial} props */ +function PokemonImage({ pokemon_id, form, gender, costume, shiny }) { + const { Icons } = useMemory.getState() + const { t } = useTranslateById() + return ( + + {t(`${pokemon_id}-${form}`)} + + ) +} + +/** @param {Partial} props */ +function RaidImage({ + raid_pokemon_id, + raid_pokemon_form, + raid_pokemon_gender, + raid_pokemon_costume, + raid_pokemon_evolution, + raid_pokemon_alignment, +}) { + const { Icons } = useMemory.getState() + const { t } = useTranslateById() + return ( + + {t(`${raid_pokemon_id}-${raid_pokemon_form}`)} + + ) +} + +/** @param {Partial} props */ +function LureImage({ lure_id }) { + const { Icons } = useMemory.getState() + const { t } = useTranslateById() + return ( + + {t(`lure_${lure_id}`)} + + ) +} + +/** @param {{ nest_pokemon_id: number, nest_pokemon_form?: number }} props */ +function NestImage({ nest_pokemon_id, nest_pokemon_form }) { + const { Icons } = useMemory.getState() + const { t } = useTranslateById() + return ( + + {t(`${nest_pokemon_id}-${nest_pokemon_form + + ) +} + +function Misc() { + const { searchTab } = useStorage.getState() + const miscIcon = useMemory((s) => s.Icons.getMisc(searchTab)) + return {searchTab} +} + +/** @param {Partial | { id: string, url?: string } | Partial | Partial | Partial | { id: string, nest_pokemon_id: number, nest_pokemon_form?: number }} props */ +function OptionImage(props) { + if ('url' in props && props.url) return + if ('quest_reward_type' in props) return + if ('pokemon_id' in props) return + if ('raid_pokemon_id' in props) return + if ('lure_id' in props) return + if ('nest_pokemon_id' in props) return + return +} + +export const OptionImageMemo = React.memo( + OptionImage, + (prev, next) => prev.id === next.id, +) diff --git a/src/components/layout/dialogs/search/index.jsx b/src/components/layout/dialogs/search/index.jsx new file mode 100644 index 000000000..776d6b630 --- /dev/null +++ b/src/components/layout/dialogs/search/index.jsx @@ -0,0 +1,114 @@ +// @ts-check +import * as React from 'react' +import Dialog from '@mui/material/Dialog' +import Box from '@mui/material/Box' +import Popper from '@mui/material/Popper' +import Autocomplete from '@mui/material/Autocomplete' +import { useTranslation } from 'react-i18next' + +import { useMemory } from '@hooks/useMemory' +import { useLayoutStore } from '@hooks/useLayoutStore' +import { useStorage } from '@hooks/useStorage' +import Utility from '@services/Utility' +import { fromSearchCategory } from '@services/functions/fromSearchCategory' +import { useMapStore } from '@hooks/useMapStore' + +import Header from '../../general/Header' +import { renderInput } from './renderInput' +import { renderOption } from './renderOption' +import { useSendSearch } from './useSendSearch' + +/** @type {import('@mui/material').AutocompleteProps['PopperComponent']} */ +const PopperComponent = ({ children, ...props }) => ( + + {children} + +) + +const handleClose = () => useLayoutStore.setState({ search: false }) + +/** @type {import('@mui/material').AutocompleteProps['onChange']} */ +const handleChange = (_, result) => { + const { map } = useMapStore.getState() + const { searchTab } = useStorage.getState() + handleClose() + if (typeof result === 'object' && 'lat' in result && 'lon' in result) { + map.flyTo([result.lat, result.lon], 16) + useMemory.setState({ + manualParams: { + category: fromSearchCategory(searchTab), + id: result.id, + }, + }) + } +} + +const DIALOG_SX = /** @type {import('@mui/material').DialogProps['sx']} */ ({ + '& .MuiDialog-container': { + alignItems: 'flex-start', + }, +}) + +const BOX_WIDTH = /** @type {import('@mui/material').BoxProps['width']} */ ({ + xs: 'inherit', + sm: 500, +}) + +const STATIC_PROPS = + /** @type {Omit} */ ({ + sx: { p: 2 }, + getOptionLabel: (option) => `${option.id}-${option.with_ar}`, + filterOptions: (o) => o, + ListboxProps: { + sx: { maxHeight: '80cqh' }, + }, + PopperComponent, + onChange: handleChange, + renderInput, + renderOption, + autoComplete: false, + clearOnBlur: false, + fullWidth: true, + clearIcon: null, + popupIcon: null, + open: true, + freeSolo: true, + }) + +export default function Search() { + Utility.analytics('/search') + + const { t } = useTranslation() + + const search = useStorage((state) => state.search) + const isMobile = useMemory((s) => s.isMobile) + const open = useLayoutStore((s) => s.search) + + const { loading, options, handleInputChange } = useSendSearch(search, open) + + return ( + + +
+ + +
+ ) +} diff --git a/src/components/layout/dialogs/search/renderInput.jsx b/src/components/layout/dialogs/search/renderInput.jsx new file mode 100644 index 000000000..9a2bc7765 --- /dev/null +++ b/src/components/layout/dialogs/search/renderInput.jsx @@ -0,0 +1,152 @@ +// @ts-check +import * as React from 'react' +import Divider from '@mui/material/Divider' +import IconButton from '@mui/material/IconButton' +import Menu from '@mui/material/Menu' +import MenuItem from '@mui/material/MenuItem' +import ListItemIcon from '@mui/material/ListItemIcon' +import ListItemText from '@mui/material/ListItemText' +import TextField from '@mui/material/TextField' +import CircularProgress from '@mui/material/CircularProgress' +import HighlightOffIcon from '@mui/icons-material/HighlightOff' +import { useTranslation } from 'react-i18next' +import { useQuery } from '@apollo/client' + +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' +import { SEARCHABLE } from '@services/queries/config' +import { Img } from '../../general/Img' + +const SearchImage = React.memo( + /** @param {{ name: string }} props */ ({ name }) => { + const { t } = useTranslation() + + const darkMode = useStorage((s) => s.darkMode) + const Icons = useMemory((s) => s.Icons) + + return ( + {t(name)} + ) + }, + (prev, next) => prev.name === next.name, +) + +const EndAdornment = React.memo( + /** @param {{ children: React.ReactNode, disabled: boolean }} props */ ({ + children, + disabled, + }) => { + const loading = useMemory((s) => s.searchLoading) + return ( + <> + useStorage.setState({ search: '' })} + > + {loading ? ( + ({ + color: theme.palette.getContrastText( + theme.palette.background.default, + ), + })} + /> + ) : ( + + )} + + + {children} + + ) + }, +) + +/** @param {import('@mui/material').AutocompleteRenderInputParams} props */ +export function renderInput({ InputProps, ...props }) { + const { t } = useTranslation() + + const searchTab = useStorage((s) => s.searchTab) + + const { data } = useQuery(SEARCHABLE, { + fetchPolicy: 'cache-first', + }) + + const [anchorEl, setAnchorEl] = React.useState(null) + + const handleClose = React.useCallback((/** @type {string} */ selection) => { + if (typeof selection === 'string') { + useStorage.setState({ searchTab: selection }) + } + setAnchorEl(null) + }, []) + + React.useEffect(() => { + if ( + data?.searchable?.length && + (typeof searchTab === 'number' || !data.searchable.includes(searchTab)) + ) { + useStorage.setState({ searchTab: data?.searchable[0] }) + } + }, [searchTab, data]) + + return ( + <> + + setAnchorEl(e.currentTarget)} + > + + + + ), + }} + sx={{ boxShadow: 1 }} + /> + + {(data?.searchable || []).map((option) => ( + handleClose(option)} + > + + + + + + ))} + + + ) +} diff --git a/src/components/layout/dialogs/search/renderOption.jsx b/src/components/layout/dialogs/search/renderOption.jsx new file mode 100644 index 000000000..a03181578 --- /dev/null +++ b/src/components/layout/dialogs/search/renderOption.jsx @@ -0,0 +1,98 @@ +// @ts-check +/* eslint-disable react/destructuring-assignment */ +import * as React from 'react' +import { t } from 'i18next' +import ListItem from '@mui/material/ListItem' +import ListItemIcon from '@mui/material/ListItemIcon' +import ListItemText from '@mui/material/ListItemText' + +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' +import { RawQuestTitle } from '@components/layout/general/QuestTitle' + +import { OptionImageMemo } from './OptionImage' + +/** @param {string} tab */ +const getBackupName = (tab) => { + switch (tab) { + case 'quests': + case 'pokestops': + return 'unknown_pokestop' + default: + return 'unknown_gym' + } +} + +/** @type {import('@mui/material').AutocompleteProps['renderOption']} */ +export const renderOption = (props, option) => { + const { searchTab } = useStorage.getState() + const { distanceUnit, questMessage } = useMemory.getState().config.misc + + return ( + ({ + backgroundColor: + option.index % 2 + ? theme.palette.background.default + : theme.palette.grey[theme.palette.mode === 'light' ? 100 : 900], + })} + {...props} + > + + + + + ) : option.lure_expire_timestamp ? ( + new Date(option.lure_expire_timestamp * 1000).toLocaleString() + ) : ( + '' + ) + } + primaryTypographyProps={{ + variant: 'subtitle2', + noWrap: true, + pr: 1, + }} + secondaryTypographyProps={{ + variant: 'caption', + noWrap: true, + pr: 1, + }} + sx={{ flexGrow: 1, flexShrink: 1 }} + /> + + + ) +} diff --git a/src/components/layout/dialogs/search/useSendSearch.js b/src/components/layout/dialogs/search/useSendSearch.js new file mode 100644 index 000000000..1a43c6679 --- /dev/null +++ b/src/components/layout/dialogs/search/useSendSearch.js @@ -0,0 +1,111 @@ +import { useState, useCallback, useMemo, useEffect } from 'react' +import { useLazyQuery } from '@apollo/client' +import { useTranslation } from 'react-i18next' +import debounce from 'lodash.debounce' + +import Query from '@services/Query' +import Utility from '@services/Utility' +import { useStorage } from '@hooks/useStorage' +import { useMemory } from '@hooks/useMemory' +import { useMapStore } from '@hooks/useMapStore' + +/** + * @param {string} search + * @param {boolean} open + * @returns {{ + * loading: boolean + * options: import('@mui/material').AutocompleteProps['options'], + * handleInputChange: import('@mui/material').AutocompleteProps['onInputChange'] + * }} + */ +export function useSendSearch(search, open) { + const [options, setOptions] = useState([]) + const searchTab = useStorage((s) => s.searchTab) + + const { i18n } = useTranslation() + const { map } = useMapStore() + + const [callSearch, { data, previousData, loading }] = useLazyQuery( + Query.search(searchTab), + ) + + const sendToServer = useCallback( + (/** @type {string} */ newSearch) => { + const { lat, lng } = map.getCenter() + const { areas } = useStorage.getState().filters.scanAreas?.filter || { + areas: [], + } + callSearch({ + variables: { + search: newSearch, + category: searchTab, + lat, + lon: lng, + locale: i18n.language, + onlyAreas: areas || [], + }, + }) + }, + [i18n.language, searchTab], + ) + + const debounceChange = useMemo( + () => debounce(sendToServer, 250), + [sendToServer], + ) + + /** @type {import('@mui/material').AutocompleteProps['onInputChange']} */ + const handleInputChange = useCallback( + (e, newValue) => { + if ( + e?.type === 'change' && + (/^[0-9\s\p{L}]+$/u.test(newValue) || newValue === '') + ) { + useStorage.setState({ search: newValue.toLowerCase() }) + debounceChange(newValue.toLowerCase()) + } + }, + [debounceChange], + ) + + useEffect(() => { + setOptions( + search + ? ( + (data || previousData)?.[ + searchTab === 'quests' + ? 'searchQuest' + : searchTab === 'lures' + ? 'searchLure' + : 'search' + ] || [] + ).map((option, index) => ({ ...option, index })) + : [], + ) + Utility.analytics('Global Search', `Search Value: ${search}`, searchTab) + }, [data]) + + useEffect(() => { + useMemory.setState({ searchLoading: loading }) + }, [loading]) + + useEffect(() => { + // initial search + if (search && open) sendToServer(search) + if (open) map.closePopup() + }, [open]) + + useEffect(() => { + if (open) { + sendToServer(search) + } + }, [searchTab]) + + useEffect(() => { + if (search === '' && open) { + setOptions([]) + } + }, [search, open]) + + return { loading, options, handleInputChange } +} diff --git a/src/components/layout/general/QuestTitle.jsx b/src/components/layout/general/QuestTitle.jsx index bdb76076f..fcce57693 100644 --- a/src/components/layout/general/QuestTitle.jsx +++ b/src/components/layout/general/QuestTitle.jsx @@ -6,14 +6,19 @@ import { useTranslation } from 'react-i18next' /** * @param {{ questTitle: string, questTarget: number }} props */ -export default function QuestTitle({ questTitle, questTarget }) { - const { t, i18n } = useTranslation() - - const normalized = `quest_title_${questTitle.toLowerCase()}` - - return i18n.exists(normalized) ? ( +export default function QuestTitle(props) { + return ( - {t(normalized, { amount_0: questTarget })} + - ) : null + ) +} + +/** + * @param {{ questTitle: string, questTarget: number }} props + */ +export function RawQuestTitle({ questTitle, questTarget }) { + const { t, i18n } = useTranslation() + const normalized = `quest_title_${questTitle.toLowerCase()}` + return i18n.exists(normalized) ? t(normalized, { amount_0: questTarget }) : '' } From 63549eb284c72ac71e4201dff4efd84b3614e6f7 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:06:43 -0500 Subject: [PATCH 10/15] feat: invasion searching --- packages/locales/lib/human/en.json | 1 + server/src/configs/default.json | 3 +- server/src/graphql/resolvers.js | 10 ++ server/src/graphql/typeDefs/index.graphql | 8 + server/src/graphql/typeDefs/map.graphql | 18 ++ server/src/models/Pokestop.js | 167 ++++++++++++++---- .../layout/dialogs/search/OptionImage.jsx | 19 +- .../layout/dialogs/search/renderOption.jsx | 83 ++++++++- .../layout/dialogs/search/useSendSearch.js | 2 + src/components/popups/Pokestop.jsx | 25 +-- src/components/popups/common/Timer.jsx | 26 ++- src/services/Query.js | 1 + src/services/functions/getGruntReward.js | 26 +++ src/services/queries/search.js | 35 ++++ 14 files changed, 351 insertions(+), 73 deletions(-) create mode 100644 src/services/functions/getGruntReward.js diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index ef9c57cc7..8411d5cf5 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -310,6 +310,7 @@ "global_search_nests": "Enter Nest Pokémon Name...", "global_search_raids": "Enter Raid Boss Name...", "global_search_pokemon": "Enter Pokemon Name...", + "global_search_invasions": "Enter Grunt Type, Name, or Pokemon reward name...", "raid_level_badges": "Raid Level Badges", "options": "Options", "profile": "Profile", diff --git a/server/src/configs/default.json b/server/src/configs/default.json index 455c1e793..bb2393501 100644 --- a/server/src/configs/default.json +++ b/server/src/configs/default.json @@ -44,7 +44,8 @@ "gyms": true, "portals": true, "nests": true, - "pokemon": true + "pokemon": true, + "invasions": true }, "queryUpdateHours": { "pokemon": 0.17, diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index 2b2ada550..d92705346 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -443,6 +443,16 @@ const resolvers = { } return [] }, + searchInvasion: (_, args, { perms, Db }) => { + const { category, search } = args + if (perms?.[category] && /^[0-9\s\p{L}]+$/u.test(search)) { + if (!search || !search.trim()) { + return [] + } + return Db.search('Pokestop', perms, args, 'searchInvasions') + } + return [] + }, searchLure: (_, args, { perms, Db }) => { const { category, search } = args if (perms?.[category] && /^[0-9\s\p{L}]+$/u.test(search)) { diff --git a/server/src/graphql/typeDefs/index.graphql b/server/src/graphql/typeDefs/index.graphql index e0feed932..c9471fff3 100644 --- a/server/src/graphql/typeDefs/index.graphql +++ b/server/src/graphql/typeDefs/index.graphql @@ -91,6 +91,14 @@ type Query { locale: String onlyAreas: [String] ): [SearchLure] + searchInvasion( + search: String + category: String + lat: Float + lon: Float + locale: String + onlyAreas: [String] + ): [SearchInvasion] searchQuest( search: String category: String diff --git a/server/src/graphql/typeDefs/map.graphql b/server/src/graphql/typeDefs/map.graphql index 74966f145..b91f4195d 100644 --- a/server/src/graphql/typeDefs/map.graphql +++ b/server/src/graphql/typeDefs/map.graphql @@ -83,6 +83,24 @@ type SearchLure { lure_expire_timestamp: Int } +type SearchInvasion { + id: ID + name: String + url: String + lat: Float + lon: Float + distance: Float + grunt_type: Int + incident_expire_timestamp: Int + confirmed: Boolean + slot_1_pokemon_id: Int + slot_1_form: Int + slot_2_pokemon_id: Int + slot_2_form: Int + slot_3_pokemon_id: Int + slot_3_form: Int +} + type SearchQuest { id: ID name: String diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index 6b6966c18..79fca9101 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -63,6 +63,54 @@ class Pokestop extends Model { return 'pokestop' } + /** + * + * @param {import('objection').QueryBuilder} query + * @param {boolean} hasMultiInvasions + * @param {boolean} isMad + * @param {boolean} multiInvasionMs + */ + static joinIncident(query, hasMultiInvasions, isMad, multiInvasionMs) { + if (hasMultiInvasions) { + if (isMad) { + query + .leftJoin('pokestop_incident', (join) => { + join + .on('pokestop.pokestop_id', '=', 'pokestop_incident.pokestop_id') + .andOn('incident_expiration', '>=', raw('UTC_TIMESTAMP()')) + }) + .select([ + 'incident_id AS incidentId', + 'pokestop_incident.character_display AS grunt_type', + 'pokestop_incident.incident_display_type AS display_type', + ]) + } else { + query + .leftJoin('incident', 'pokestop.id', 'incident.pokestop_id') + .select([ + '*', + 'pokestop.updated', + 'pokestop.id AS id', + 'incident.id AS incidentId', + raw( + multiInvasionMs + ? 'FLOOR(incident.updated_ms / 1000) AS incident_updated' + : 'incident.updated AS incident_updated', + ), + raw( + multiInvasionMs + ? 'FLOOR(incident.expiration_ms / 1000) AS incident_expire_timestamp' + : 'incident.expiration AS incident_expire_timestamp', + ), + 'incident.character AS grunt_type', + ]) + } + } else if (isMad) { + query.select('incident_grunt_type AS grunt_type') + } + return query + } + static async getAll( perms, args, @@ -137,43 +185,7 @@ class Pokestop extends Model { } else if (hideOldPokestops) { query.where('pokestop.updated', '>', ts - stopValidDataLimit * 86400) } - if (hasMultiInvasions) { - if (isMad) { - query - .leftJoin('pokestop_incident', (join) => { - join - .on('pokestop.pokestop_id', '=', 'pokestop_incident.pokestop_id') - .andOn('incident_expiration', '>=', raw('UTC_TIMESTAMP()')) - }) - .select([ - 'incident_id AS incidentId', - 'pokestop_incident.character_display AS grunt_type', - 'pokestop_incident.incident_display_type AS display_type', - ]) - } else { - query - .leftJoin('incident', 'pokestop.id', 'incident.pokestop_id') - .select([ - '*', - 'pokestop.updated', - 'pokestop.id AS id', - 'incident.id AS incidentId', - raw( - multiInvasionMs - ? 'FLOOR(incident.updated_ms / 1000) AS incident_updated' - : 'incident.updated AS incident_updated', - ), - raw( - multiInvasionMs - ? 'FLOOR(incident.expiration_ms / 1000) AS incident_expire_timestamp' - : 'incident.expiration AS incident_expire_timestamp', - ), - 'incident.character AS grunt_type', - ]) - } - } else if (isMad) { - query.select('incident_grunt_type AS grunt_type') - } + Pokestop.joinIncident(query, hasMultiInvasions, isMad, multiInvasionMs) query .whereBetween(isMad ? 'latitude' : 'lat', [args.minLat, args.maxLat]) .andWhereBetween(isMad ? 'longitude' : 'lon', [args.minLon, args.maxLon]) @@ -1823,6 +1835,87 @@ class Pokestop extends Model { return results } + static async searchInvasions( + perms, + args, + { isMad, hasMultiInvasions, multiInvasionMs, hasConfirmed }, + distance, + bbox, + ) { + const { search, onlyAreas = [], locale } = args + const ts = Math.floor(Date.now() / 1000) + + const invasions = Object.keys(Event.invasions).filter((invasion) => + i18next + .t(`grunt_${invasion}`, { lng: locale }) + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .includes(search), + ) + const validMons = new Set( + Event.available.pokestops.filter((a) => a.startsWith('a')), + ) + const pokemonIds = Object.keys(Event.masterfile.pokemon) + .filter( + (pkmn) => + i18next + .t(`poke_${pkmn}`, { lng: locale }) + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .includes(search) && + (validMons.has(`a${pkmn}-0`) || + Object.keys(Event.masterfile.pokemon[pkmn].forms || {}).some( + (form) => validMons.has(`a${pkmn}-${form}`), + )), + ) + .map((x) => +x) + if (!invasions.length && !pokemonIds.length) { + return [] + } + const query = this.query() + .whereBetween(isMad ? 'latitude' : 'lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween(isMad ? 'longitude' : 'lon', [bbox.minLon, bbox.maxLon]) + .limit(searchResultsLimit) + .orderBy('distance') + + Pokestop.joinIncident(query, hasMultiInvasions, isMad, multiInvasionMs) + query.select(distance) + + if (isMad) { + query.whereRaw('incident_expiration > UTC_TIMESTAMP()') + } else { + query.andWhere('expiration', '>=', ts) + } + if (invasions.length) { + query.whereIn(isMad ? 'character_display' : 'character', invasions) + } + if (hasConfirmed && pokemonIds.length) { + query.where((subQuery) => { + subQuery + .whereIn('slot_1_pokemon_id', pokemonIds) + .orWhereIn('slot_2_pokemon_id', pokemonIds) + .orWhereIn('slot_3_pokemon_id', pokemonIds) + }) + } + if (!getAreaSql(query, perms.areaRestrictions, onlyAreas, isMad)) { + return [] + } + const results = await query + return pokemonIds.length + ? results.filter(({ grunt_type }) => + ['first', 'second', 'third'].some( + (pos) => + Event.invasions[grunt_type]?.[`${pos}Reward`] && + Event.invasions[grunt_type]?.encounters[pos]?.some((pkmn) => + pokemonIds.includes(pkmn.id), + ), + ), + ) + : results + } + static getOne(id, { isMad }) { return this.query() .select([ diff --git a/src/components/layout/dialogs/search/OptionImage.jsx b/src/components/layout/dialogs/search/OptionImage.jsx index c98c74349..75b154e5f 100644 --- a/src/components/layout/dialogs/search/OptionImage.jsx +++ b/src/components/layout/dialogs/search/OptionImage.jsx @@ -149,13 +149,29 @@ function NestImage({ nest_pokemon_id, nest_pokemon_form }) { ) } +/** @param {Partial} props */ +function InvasionImage({ grunt_type, confirmed }) { + const { Icons } = useMemory.getState() + const { t } = useTranslateById() + return ( + + {t(`grunt_${grunt_type}`)} + + ) +} + function Misc() { const { searchTab } = useStorage.getState() const miscIcon = useMemory((s) => s.Icons.getMisc(searchTab)) return {searchTab} } -/** @param {Partial | { id: string, url?: string } | Partial | Partial | Partial | { id: string, nest_pokemon_id: number, nest_pokemon_form?: number }} props */ +/** @param {Partial | { id: string, url?: string } | Partial | Partial | Partial | { id: string, nest_pokemon_id: number, nest_pokemon_form?: number } | Partial & { id: string }} props */ function OptionImage(props) { if ('url' in props && props.url) return if ('quest_reward_type' in props) return @@ -163,6 +179,7 @@ function OptionImage(props) { if ('raid_pokemon_id' in props) return if ('lure_id' in props) return if ('nest_pokemon_id' in props) return + if ('grunt_type' in props) return return } diff --git a/src/components/layout/dialogs/search/renderOption.jsx b/src/components/layout/dialogs/search/renderOption.jsx index a03181578..ed36968d4 100644 --- a/src/components/layout/dialogs/search/renderOption.jsx +++ b/src/components/layout/dialogs/search/renderOption.jsx @@ -5,11 +5,18 @@ import { t } from 'i18next' import ListItem from '@mui/material/ListItem' import ListItemIcon from '@mui/material/ListItemIcon' import ListItemText from '@mui/material/ListItemText' +import Grid2 from '@mui/material/Unstable_Grid2/Grid2' +import { Img } from '@components/layout/general/Img' import { useMemory } from '@hooks/useMemory' import { useStorage } from '@hooks/useStorage' import { RawQuestTitle } from '@components/layout/general/QuestTitle' +import { Divider, Typography } from '@mui/material' +import Utility from '@services/Utility' +import { RawTimeSince } from '@components/popups/common/Timer' +import { getGruntReward } from '@services/functions/getGruntReward' + import { OptionImageMemo } from './OptionImage' /** @param {string} tab */ @@ -23,6 +30,65 @@ const getBackupName = (tab) => { } } +const MiniInvasion = ({ id, form }) => { + const { Icons } = useMemory.getState() + return +} +/** @param {import('@rm/types').Invasion} props */ +const InvasionSubtitle = ({ + confirmed, + grunt_type, + slot_1_pokemon_id, + slot_1_form, + slot_2_pokemon_id, + slot_2_form, + slot_3_pokemon_id, + slot_3_form, + incident_expire_timestamp, +}) => { + const expire = Utility.getTimeUntil(incident_expire_timestamp * 1000, true) + if (!confirmed) return expire.str + const { masterfile } = useMemory.getState() + const reward = getGruntReward(masterfile.invasions[grunt_type]) + + return ( + + {slot_1_pokemon_id && ( + <> + + {!!reward.first && ( + +  {`(${reward.first}%)`} + + )} + + + )} + {slot_2_pokemon_id && ( + <> + + {!!reward.second && ( + +  {`(${reward.second}%)`} + + )} + + + )} + {slot_3_pokemon_id && ( + <> + + {!!reward.third && ( + +  {`(${reward.third}%)`} + + )} + + )} + + ) +} + /** @type {import('@mui/material').AutocompleteProps['renderOption']} */ export const renderOption = (props, option) => { const { searchTab } = useStorage.getState() @@ -49,6 +115,8 @@ export const renderOption = (props, option) => { ? `(${t(`form_${option.form}`)})` : '' }${option.iv ? ` - ${option.iv}%` : ''}` + : option.grunt_type + ? t(`grunt_${option.grunt_type}`).toString() : option.name || t(getBackupName(searchTab)) } secondary={ @@ -58,7 +126,9 @@ export const renderOption = (props, option) => { questTarget={option.quest_target} /> ) : option.lure_expire_timestamp ? ( - new Date(option.lure_expire_timestamp * 1000).toLocaleString() + t(`lure_${option.lure_id}`).toString() + ) : option.grunt_type ? ( + ) : ( '' ) @@ -80,8 +150,15 @@ export const renderOption = (props, option) => { distanceUnit === 'mi' ? t('mi') : t('km') }`} secondary={ - searchTab === 'quests' && - (questMessage || t(`ar_quest_${!!option.with_ar}`).toString()) + searchTab === 'quests' ? ( + questMessage || t(`ar_quest_${!!option.with_ar}`).toString() + ) : searchTab === 'invasions' ? ( + + ) : searchTab === 'lures' ? ( + + ) : ( + '' + ) } primaryTypographyProps={{ variant: 'subtitle2', diff --git a/src/components/layout/dialogs/search/useSendSearch.js b/src/components/layout/dialogs/search/useSendSearch.js index 1a43c6679..709d6fa1c 100644 --- a/src/components/layout/dialogs/search/useSendSearch.js +++ b/src/components/layout/dialogs/search/useSendSearch.js @@ -77,6 +77,8 @@ export function useSendSearch(search, open) { ? 'searchQuest' : searchTab === 'lures' ? 'searchLure' + : searchTab === 'invasions' + ? 'searchInvasion' : 'search' ] || [] ).map((option, index) => ({ ...option, index })) diff --git a/src/components/popups/Pokestop.jsx b/src/components/popups/Pokestop.jsx index 6ee924de7..7c4c3c13a 100644 --- a/src/components/popups/Pokestop.jsx +++ b/src/components/popups/Pokestop.jsx @@ -24,6 +24,7 @@ import { useStorage } from '@hooks/useStorage' import Utility from '@services/Utility' import { getBadge } from '@services/functions/getBadge' import getRewardInfo from '@services/functions/getRewardInfo' +import { getGruntReward } from '@services/functions/getGruntReward' import Dropdown from './common/Dropdown' import TimeTile from './common/TimeTile' @@ -723,27 +724,6 @@ const ExtraInfo = ({ last_modified_timestamp, updated, lat, lon }) => { ) } -/** - * @typedef {import('@rm/types').Masterfile['invasions']} Invasions - * @param {Invasions[keyof Invasions]} grunt - * @returns {{ first?: string, second?: string, third?: string }} - */ -const getRewardPercent = (grunt) => { - if (grunt.type.startsWith('NPC')) { - return {} - } - if (grunt.secondReward) { - return { first: '85%', second: '15%' } - } - if (grunt.thirdReward) { - return { third: '100%' } - } - if (grunt.firstReward) { - return { first: '100%' } - } - return {} -} - /** * * @param {{ id: number, form: number, gender?: number, costumeId?: number, shiny?: boolean}} param0 @@ -804,6 +784,7 @@ const Invasion = ({ grunt_type, confirmed, ...invasion }) => { ([position, lineup], i) => { const id = invasion[`slot_${i + 1}_pokemon_id`] const form = invasion[`slot_${i + 1}_form`] + const reward = getGruntReward(info)[position] return ( {ENCOUNTER_NUM[position]} @@ -820,7 +801,7 @@ const Invasion = ({ grunt_type, confirmed, ...invasion }) => { )) )} - {getRewardPercent(info)[position] || ''} + {reward ? `${reward}%` : ''} ) }, diff --git a/src/components/popups/common/Timer.jsx b/src/components/popups/common/Timer.jsx index a7035cbf2..cffb82242 100644 --- a/src/components/popups/common/Timer.jsx +++ b/src/components/popups/common/Timer.jsx @@ -5,12 +5,8 @@ import { useTranslation } from 'react-i18next' import Utility from '@services/Utility' -/** - * - * @param {{ expireTime?: number, until?: boolean } & import('@mui/material').TypographyProps} props - * @returns - */ -export default function TimeSince({ expireTime, until = false, ...props }) { +/** @param {TimerProps} props */ +export function RawTimeSince({ expireTime, until = false }) { const { t } = useTranslation() const endTime = new Date(expireTime * 1000) const [timerEnd, setTimerEnd] = React.useState( @@ -28,11 +24,23 @@ export default function TimeSince({ expireTime, until = false, ...props }) { setTimerEnd(Utility.getTimeUntil(endTime, until)) }, [expireTime]) + return expireTime + ? timerEnd.str.replace('days', t('days')).replace('day', t('day')) + : t('never') +} + +/** + * @typedef {{ expireTime?: number, until?: boolean }} TimerProps + */ +/** + * + * @param {TimerProps & import('@mui/material').TypographyProps} props + * @returns + */ +export default function TimeSince({ expireTime, until = false, ...props }) { return ( - {expireTime - ? timerEnd.str.replace('days', t('days')).replace('day', t('day')) - : t('never')} + ) } diff --git a/src/services/Query.js b/src/services/Query.js index 507444ca7..888b92266 100644 --- a/src/services/Query.js +++ b/src/services/Query.js @@ -142,6 +142,7 @@ export default class Query { case 'raids': case 'nests': case 'quests': + case 'invasions': case 'pokemon': return searchIndex[category] case 'webhook': diff --git a/src/services/functions/getGruntReward.js b/src/services/functions/getGruntReward.js new file mode 100644 index 000000000..d475fd66d --- /dev/null +++ b/src/services/functions/getGruntReward.js @@ -0,0 +1,26 @@ +// @ts-check +/** + * @typedef {import('@rm/types').Masterfile['invasions']} Invasions + * @param {Invasions[keyof Invasions]} grunt + * @returns {{ first: number, second: number, third: number }} + */ +export const getGruntReward = (grunt) => { + const base = { first: 0, second: 0, third: 0 } + if (!grunt || grunt.type.startsWith('NPC')) { + return base + } + if (grunt.secondReward) { + base.first = 85 + base.second = 15 + return base + } + if (grunt.thirdReward) { + base.third = 100 + return base + } + if (grunt.firstReward) { + base.first = 100 + return base + } + return base +} diff --git a/src/services/queries/search.js b/src/services/queries/search.js index 313e01970..25352ae14 100644 --- a/src/services/queries/search.js +++ b/src/services/queries/search.js @@ -218,3 +218,38 @@ export const raids = gql` } } ` + +export const invasions = gql` + query SearchInvasions( + $search: String! + $category: String! + $lat: Float! + $lon: Float! + $locale: String! + $onlyAreas: [String] + ) { + searchInvasion( + search: $search + category: $category + lat: $lat + lon: $lon + locale: $locale + onlyAreas: $onlyAreas + ) { + id + name + lat + lon + distance + grunt_type + incident_expire_timestamp + confirmed + slot_1_pokemon_id + slot_1_form + slot_2_pokemon_id + slot_2_form + slot_3_pokemon_id + slot_3_form + } + } +` From bb74b2409db441031884d1959e6a1030b592c316 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:37:29 -0500 Subject: [PATCH 11/15] fix: caching --- src/components/layout/dialogs/search/useSendSearch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/layout/dialogs/search/useSendSearch.js b/src/components/layout/dialogs/search/useSendSearch.js index 709d6fa1c..86c4eef23 100644 --- a/src/components/layout/dialogs/search/useSendSearch.js +++ b/src/components/layout/dialogs/search/useSendSearch.js @@ -27,6 +27,7 @@ export function useSendSearch(search, open) { const [callSearch, { data, previousData, loading }] = useLazyQuery( Query.search(searchTab), + { fetchPolicy: 'cache-and-network' }, ) const sendToServer = useCallback( @@ -92,7 +93,6 @@ export function useSendSearch(search, open) { }, [loading]) useEffect(() => { - // initial search if (search && open) sendToServer(search) if (open) map.closePopup() }, [open]) From fbb781add772d3d91acc039d82264523d8e55f21 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 19 Jan 2024 13:46:16 -0500 Subject: [PATCH 12/15] fix: add search log tag --- packages/logger/lib/index.js | 1 + server/src/models/Pokemon.js | 1 + server/src/services/DbCheck.js | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/logger/lib/index.js b/packages/logger/lib/index.js index f46789717..4ce8f0f45 100644 --- a/packages/logger/lib/index.js +++ b/packages/logger/lib/index.js @@ -55,6 +55,7 @@ const HELPERS = /** @type {const} */ ({ portals: chalk.hex('#795548')('[PORTALS]'), route: chalk.hex('#607d8b')('[ROUTE]'), routes: chalk.hex('#9e9e9e')('[ROUTES]'), + search: chalk.hex('#795548')('[SEARCH]'), custom: (text = '', color = '#64b5f6') => chalk.hex(color)(`[${text.toUpperCase()}]`), diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index b5bb15e8c..9c15c47aa 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -647,6 +647,7 @@ class Pokemon extends Model { 'POST', secret, ) + if (!results) return [] return results .filter( (item) => !mem || filterRTree(item, perms.areaRestrictions, onlyAreas), diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index 0609efa6d..817d78214 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -491,7 +491,7 @@ module.exports = class DbCheck { } if (count > 1) { log.info( - HELPERS.db, + HELPERS.search, 'Searched', count, '| received:', From a0d1e6c18f2533ac2220618ef6dce31f57949c9a Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:11:01 -0500 Subject: [PATCH 13/15] fix: remove unused --- server/src/models/Pokemon.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index 9c15c47aa..a54387127 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -568,17 +568,9 @@ class Pokemon extends Model { * @param {import("@rm/types").DbContext} ctx * @param {number} distance * @param {ReturnType} bbox - * @param {number} [maxDistance] * @returns {Promise[]>} */ - static async search( - perms, - args, - { isMad, mem, secret }, - distance, - bbox, - maxDistance, - ) { + static async search(perms, args, { isMad, mem, secret }, distance, bbox) { const { search, locale, onlyAreas = [] } = args const pokemonIds = Object.keys(Event.masterfile.pokemon).filter((pkmn) => i18next.t(`poke_${pkmn}`, { lng: locale }).toLowerCase().includes(search), @@ -637,7 +629,6 @@ class Pokemon extends Model { latitude: bbox.maxLat, longitude: bbox.maxLon, }, - maxDistance, limit: searchResultsLimit, searchIds: pokemonIds.map((id) => +id), global: {}, From b088b7b2d2af2aaccbcd9b9808772b0b2e6eb312 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:50:20 -0500 Subject: [PATCH 14/15] fix: compare array sizes before updating --- server/src/services/DbCheck.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/services/DbCheck.js b/server/src/services/DbCheck.js index 817d78214..a7574f2c0 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbCheck.js @@ -448,7 +448,6 @@ module.exports = class DbCheck { const loopTime = Date.now() count += 1 const bbox = getBboxFromCenter(args.lat, args.lon, distance) - const local = distance const data = await Promise.all( this.models[model].map(async ({ SubModel, ...source }) => SubModel[method]( @@ -457,11 +456,13 @@ module.exports = class DbCheck { source, this.getDistance(args, source.isMad), bbox, - local, ), ), ) - deDuped = DbCheck.deDupeResults(data) + const results = DbCheck.deDupeResults(data) + if (results.length > deDuped.length) { + deDuped = results + } log.debug( HELPERS.db, 'Search attempt #', From 50b06c3553d69539bab59e7a5c7e449984151dbe Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:55:41 -0500 Subject: [PATCH 15/15] fix: showing feedback --- src/components/layout/dialogs/search/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/layout/dialogs/search/index.jsx b/src/components/layout/dialogs/search/index.jsx index 776d6b630..a2df51a3a 100644 --- a/src/components/layout/dialogs/search/index.jsx +++ b/src/components/layout/dialogs/search/index.jsx @@ -76,7 +76,6 @@ const STATIC_PROPS = clearIcon: null, popupIcon: null, open: true, - freeSolo: true, }) export default function Search() { @@ -100,6 +99,7 @@ export default function Search() {