diff --git a/commands/interaction/slash/search/anime.js b/commands/interaction/slash/search/anime.js index a7486a6..0f1f59a 100644 --- a/commands/interaction/slash/search/anime.js +++ b/commands/interaction/slash/search/anime.js @@ -2,7 +2,8 @@ const { anime } = require('#api'); const { paginator } = require('#client'); const { PERMISSION_GROUPS, OMNI_ANIME_FORMAT_TYPES } = require('#constants'); -const { createEmbed, formatPaginationEmbeds, page, hexToEmbedColor } = require('#utils/embed'); +const { hexToDecimalColor } = require("#utils/color"); +const { createEmbed, formatPaginationEmbeds, page } = require('#utils/embed'); const { acknowledge } = require('#utils/interactions'); const { smallPill, link, pill, stringwrapPreserveWords} = require('#utils/markdown'); const { editOrReply } = require('#utils/message'); @@ -38,7 +39,7 @@ function renderAnimeResultsPage(context, res){ if(res.image) result.image = { url: res.image }; // Render Color - if(res.color) result.color = hexToEmbedColor(res.color); + if(res.color) result.color = hexToDecimalColor(res.color); // Render Episode Metadata if(res.episodes) { diff --git a/commands/interaction/slash/search/maps.js b/commands/interaction/slash/search/maps.js index d76dd87..40bf0e0 100644 --- a/commands/interaction/slash/search/maps.js +++ b/commands/interaction/slash/search/maps.js @@ -1,7 +1,8 @@ const { maps, mapsSupplemental } = require('#api'); const { PERMISSION_GROUPS } = require('#constants'); -const { createEmbed, hexToEmbedColor } = require('#utils/embed'); +const { hexToDecimalColor } = require("#utils/color"); +const { createEmbed } = require('#utils/embed'); const { acknowledge } = require('#utils/interactions'); const { link, icon, iconAsEmojiObject, citation } = require('#utils/markdown'); const { editOrReply } = require('#utils/message') @@ -19,7 +20,7 @@ function renderPlaceCard(context, place) { }, description: `${place.address.full}`, url: place.url, - color: hexToEmbedColor(place.style.color) + color: hexToDecimalColor(place.style.color) })] if (place.display_type) { diff --git a/commands/interaction/slash/search/movie.js b/commands/interaction/slash/search/movie.js index d41038d..fb734fc 100644 --- a/commands/interaction/slash/search/movie.js +++ b/commands/interaction/slash/search/movie.js @@ -2,7 +2,8 @@ const { movie } = require('#api'); const { paginator } = require('#client'); const { PERMISSION_GROUPS, OMNI_ANIME_FORMAT_TYPES, OMNI_MOVIE_TYPES } = require('#constants'); -const { createEmbed, formatPaginationEmbeds, page, hexToEmbedColor } = require('#utils/embed'); +const { hexToDecimalColor } = require("#utils/color"); +const { createEmbed, formatPaginationEmbeds, page } = require('#utils/embed'); const { acknowledge } = require('#utils/interactions'); const { smallPill, pill } = require('#utils/markdown'); const { editOrReply } = require('#utils/message'); @@ -29,7 +30,7 @@ function renderMovieResultsPage(context, res){ if(res.image) result.image = { url: res.image }; // Render Color - if(res.color) result.color = hexToEmbedColor(res.color); + if(res.color) result.color = hexToDecimalColor(res.color); return page(result); } diff --git a/commands/message/dev/test.js b/commands/message/dev/test.js index 03cc7f7..b65ff4f 100644 --- a/commands/message/dev/test.js +++ b/commands/message/dev/test.js @@ -30,6 +30,7 @@ module.exports = { label: "single sub page", inline: true, visible: true, + disableCache: true, resolvePage: ()=>{ return [ createEmbed("success", context, "smiley") @@ -42,8 +43,6 @@ module.exports = { // Next to pagination or new row inline: false, visible: (page) => { - console.log(page.getAllState()); - console.log(page.getState("t_1")); return (page.getState("key") === "t_1") }, resolvePage: (pg) => { @@ -76,7 +75,55 @@ module.exports = { createEmbed("default", context, { description: "this is a dynamic sub page " + Math.random()}) ].map((p)=>page(p)); } - } + }, + conditional_button_2: { + // Button Label + label: "Conditional", + // Next to pagination or new row + inline: false, + visible: true, + resolvePage: (pg) => {throw "a"} + }, + conditional_button_3: { + // Button Label + label: "Conditional", + // Next to pagination or new row + inline: false, + visible: true, + resolvePage: (pg) => {throw "a"} + }, + conditional_button_4: { + // Button Label + label: "Conditional", + // Next to pagination or new row + inline: false, + visible: true, + resolvePage: (pg) => {throw "a"} + }, + conditional_button_5: { + // Button Label + label: "Conditional", + // Next to pagination or new row + inline: false, + visible: true, + resolvePage: (pg) => {throw "a"} + }, + conditional_button_6: { + // Button Label + label: "Conditional", + // Next to pagination or new row + inline: false, + visible: true, + resolvePage: (pg) => {throw "a"} + }, + conditional_button_7: { + // Button Label + label: "Conditional", + // Next to pagination or new row + inline: false, + visible: true, + resolvePage: (pg) => {throw "a"} + }, } }) }catch(e){ diff --git a/commands/message/search/anime.js b/commands/message/search/anime.js index c87d8f0..441c4d6 100644 --- a/commands/message/search/anime.js +++ b/commands/message/search/anime.js @@ -1,13 +1,15 @@ const { anime } = require('#api'); const { paginator } = require('#client'); -const { PERMISSION_GROUPS, OMNI_ANIME_FORMAT_TYPES } = require('#constants'); +const { PERMISSION_GROUPS, OMNI_ANIME_FORMAT_TYPES, COLORS, COLORS_HEX} = require('#constants'); -const { createEmbed, formatPaginationEmbeds, page, hexToEmbedColor } = require('#utils/embed'); +const { hexToDecimalColor } = require("#utils/color"); +const { createEmbed, formatPaginationEmbeds, page } = require('#utils/embed'); const { acknowledge } = require('#utils/interactions'); const { smallPill, link, pill, stringwrap, stringwrapPreserveWords} = require('#utils/markdown'); const { editOrReply } = require('#utils/message'); const DynamicCardStack = require("../../../labscore/cardstack/DynamicCardStack"); +const {STATIC_ASSETS} = require("#utils/statics"); function renderAnimeResultsPage(context, res){ let result = createEmbed("default", context, { @@ -39,7 +41,7 @@ function renderAnimeResultsPage(context, res){ if(res.image) result.image = { url: res.image }; // Render Color - if(res.color) result.color = hexToEmbedColor(res.color); + if(res.color) result.color = hexToDecimalColor(res.color); // Render Episode Metadata if(res.episodes) { @@ -60,7 +62,8 @@ function renderAnimeResultsPage(context, res){ return page(result, {}, { episodes_key: res.supplemental.episodes, - name: res.title + name: res.title, + color: hexToDecimalColor(res.color || COLORS_HEX.embed) }); } @@ -111,10 +114,11 @@ module.exports = { }, renderLoadingState: (pg) => { return createEmbed("default", context, { - description: `-# ${pg.getState("name")} > **Episodes**`, + description: `-# ${pg.getState("name")} › **Episodes**`, image: { - url: `https://bignutty.gitlab.io/webstorage4/v2/assets/loading/04_chat_loading.1zn1ocfb72tc.gif` - } + url: STATIC_ASSETS.card_skeleton + }, + color: pg.getState("color") }) }, // Will resolve all conditions at paginator creation time @@ -130,13 +134,16 @@ module.exports = { let i = 0; return [ createEmbed("default", context, { - description: `-# ${pg.getState("name")} > **Episodes**\n\nepisode page ${i++}`, + description: `-# ${pg.getState("name")} › **Episodes**\n\nepisode page ${i++}`, + color: pg.getState("color") }), createEmbed("default", context, { - description: `-# ${pg.getState("name")} > **Episodes**\n\nepisode page ${i++}`, + description: `-# ${pg.getState("name")} › **Episodes**\n\nepisode page ${i++}`, + color: pg.getState("color") }), createEmbed("default", context, { - description: `-# ${pg.getState("name")} > **Episodes**\n\nepisode page ${i++}`, + description: `-# ${pg.getState("name")} › **Episodes**\n\nepisode page ${i++}`, + color: pg.getState("color") }) ].map((p)=>page(p)); } diff --git a/commands/message/search/maps.js b/commands/message/search/maps.js index fff926e..5c11a45 100644 --- a/commands/message/search/maps.js +++ b/commands/message/search/maps.js @@ -1,7 +1,8 @@ const { maps, mapsSupplemental } = require('#api'); const { PERMISSION_GROUPS } = require('#constants'); -const { createEmbed, hexToEmbedColor } = require('#utils/embed'); +const { hexToDecimalColor } = require("#utils/color"); +const { createEmbed } = require('#utils/embed'); const { acknowledge } = require('#utils/interactions'); const { link, icon, iconAsEmojiObject, citation, stringwrap } = require('#utils/markdown'); const { editOrReply } = require('#utils/message') @@ -18,7 +19,7 @@ function renderPlaceCard(context, place) { }, description: `${place.address.full}`, url: place.url, - color: hexToEmbedColor(place.style.color) + color: hexToDecimalColor(place.style.color) })] if (place.display_type) { diff --git a/commands/message/search/movie.js b/commands/message/search/movie.js index 47486c8..4cf92de 100644 --- a/commands/message/search/movie.js +++ b/commands/message/search/movie.js @@ -2,7 +2,8 @@ const { movie } = require('#api'); const { paginator } = require('#client'); const { PERMISSION_GROUPS, OMNI_ANIME_FORMAT_TYPES, OMNI_MOVIE_TYPES } = require('#constants'); -const { createEmbed, formatPaginationEmbeds, page, hexToEmbedColor } = require('#utils/embed'); +const { hexToDecimalColor } = require("#utils/color"); +const { createEmbed, formatPaginationEmbeds, page } = require('#utils/embed'); const { acknowledge } = require('#utils/interactions'); const { smallPill, pill } = require('#utils/markdown'); const { editOrReply } = require('#utils/message'); @@ -28,7 +29,7 @@ function renderMovieResultsPage(context, res){ if(res.image) result.image = { url: res.image }; // Render Color - if(res.color) result.color = hexToEmbedColor(res.color); + if(res.color) result.color = hexToDecimalColor(res.color); return page(result); } diff --git a/labscore/cardstack/DynamicCardStack.js b/labscore/cardstack/DynamicCardStack.js index 771d2f1..873ee01 100644 --- a/labscore/cardstack/DynamicCardStack.js +++ b/labscore/cardstack/DynamicCardStack.js @@ -5,6 +5,7 @@ const { MessageComponentTypes, InteractionCallbackTypes } = require("detritus-cl const { Message } = require("detritus-client/lib/structures"); const { ComponentContext, Components, ComponentActionRow} = require("detritus-client/lib/utils"); const {createEmbed, page} = require("#utils/embed"); +const {STATIC_ASSETS} = require("#utils/statics"); const activeStacks = new Map(); @@ -19,6 +20,10 @@ const SUBCATEGORY_STATE_TYPES = Object.freeze({ MULTI_PAGE: 2, }); +const STACK_CACHE_KEYS = Object.freeze({ + RESULT_CARDS: 0 +}) + /** * DynamicCardStack represents an interactive stacks * of cards (embeds) for the user to paginate through @@ -35,6 +40,7 @@ class DynamicCardStack { * @param {Number} options.startingIndex Starting card index * @param {boolean} options.loop Wrap paging * @param {number} options.expires CardStack timeout + * @param {boolean} options.disableStackCache Allows disabling the stack result cache, meaning that every trigger will reevaluate a stack */ constructor(context, options){ this.context = context; @@ -49,10 +55,14 @@ class DynamicCardStack { this.uniqueId = (Date.now()*Math.random()).toString(36); + this.stackCache = {}; this.pageState = []; this.subcategoryState = SUBCATEGORY_STATE_TYPES.SINGLE_PAGE; this.currentSelectedSubcategory = null; + this.lastInteraction = Date.now(); + this.spawned = 0; + console.log("now spawning " + this.uniqueId) this._spawn(); } @@ -64,6 +74,7 @@ class DynamicCardStack { console.log("killing " + this.uniqueId) clearTimeout(this.timeout); + this.listener.clear(); if(clearComponents) await this._edit(this.currentPage(), []) // Remove reference to free the cardstack for GC @@ -89,6 +100,23 @@ class DynamicCardStack { activeStacks.set(this.context.message?.id, this); } + _createTimeout(){ + return setTimeout(()=>{ + /* + // This currently isn't doable with our Components listener. + if(this.spawned - this.lastInteraction <= this.expires){ + clearTimeout(this.timeout) + this.spawned = Date.now(); + // New expiry time is 30 seconds + this.expires = 30*1000; + this.timeout = this._createTimeout(); + } else { + this.kill(true); + }*/ + this.kill(true); + }, this.expires) + } + /** * Creates a new cardstack in the given channel */ @@ -116,10 +144,9 @@ class DynamicCardStack { } }) - this.timeout = setTimeout(()=>{ - console.log(this.uniqueId + " timed out.") - this.kill(true); - }, this.expires) + this.timeout = this._createTimeout() + + this.spawned = Date.now() return this._edit({ ...this.getCurrentCard(), @@ -221,7 +248,7 @@ class DynamicCardStack { */ _renderComponents(disabled = false){ let nComponents = new ComponentActionRow({}) - let nComponentsSecondary = new ComponentActionRow({}) + let nComponentsSecondary = [new ComponentActionRow({})] // First Row always starts with built-in components for(const b of this.buttons){ @@ -251,7 +278,7 @@ class DynamicCardStack { disabled: disabled } - if(button.condition && typeof(button.condition) == "function") component.disabled = !button.condition(this); + if(!disabled && button.condition && typeof(button.condition) == "function") component.disabled = !button.condition(this); if(button.label){ if(typeof(button.label) === "function") component.label = button.label(this); @@ -263,19 +290,43 @@ class DynamicCardStack { if(this.currentSelectedSubcategory === b) component.style = 1; if(button.inline) nComponents.addButton(component); - else nComponentsSecondary.addButton(component); + else { + if(nComponentsSecondary[nComponentsSecondary.length - 1].components.length >= 5){ + nComponentsSecondary.push(new ComponentActionRow({})) + } + nComponentsSecondary[nComponentsSecondary.length - 1].addButton(component); + } } - if(nComponentsSecondary.components.length >= 1) return [nComponents, nComponentsSecondary] + if(nComponentsSecondary[0].components.length >= 1) return [nComponents, ...nComponentsSecondary] return [nComponents]; } + _setCachedValue(index, componentId, key, value){ + if(!this.stackCache[index]) this.stackCache[index] = {}; + if(!this.stackCache[index][componentId]) this.stackCache[index][componentId] = {}; + this.stackCache[index][componentId][key] = value; + } + + _getCachedValue(index, componentId, key){ + if(!this.stackCache[index]) return null; + if(!this.stackCache[index][componentId]) return null; + if(!this.stackCache[index][componentId][key]) return null; + return this.stackCache[index][componentId][key]; + } + /** * Handles an interaction from the attached components * @param {ComponentContext} ctx */ async _handleInteraction(ctx){ + if(ctx.user.id !== this.context.user.id) return ctx.respond({ + type: InteractionCallbackTypes.DEFERRED_UPDATE_MESSAGE + }) + + this.lastInteraction = Date.now(); + // should be a built-in button if(["next","previous"].includes(ctx.data.customId)){ console.log("triggering button") @@ -311,21 +362,38 @@ class DynamicCardStack { } else this.currentSelectedSubcategory = ctx.data.customId; - this.cachedIndex = this.index; + console.log("new category is " + this.currentSelectedSubcategory) - let processingEmbed = page(createEmbed("default", ctx, { - "description": "looading..." - })) - - if(this.interactive_components[ctx.data.customId].renderLoadingState) processingEmbed = page(this.interactive_components[ctx.data.customId].renderLoadingState(this)); - await ctx.respond({ - type: InteractionCallbackTypes.UPDATE_MESSAGE, - data: Object.assign(processingEmbed, { components: this._renderComponents(true)}) - }) + // TODO: allow overriding index + this.index = 0; try{ - // TODO: cache resolved stacks - this.activeCardStack = await this.interactive_components[ctx.data.customId].resolvePage(this); + if(this._getCachedValue(this.rootIndex, ctx.data.customId, STACK_CACHE_KEYS.RESULT_CARDS) !== null){ + this.activeCardStack = [...this._getCachedValue(this.rootIndex, ctx.data.customId, STACK_CACHE_KEYS.RESULT_CARDS)]; + await ctx.respond({ + type: InteractionCallbackTypes.UPDATE_MESSAGE, + data: Object.assign(this.currentPage(), {components:this._renderComponents(false)}) + }) + return; + } else { + let processingEmbed = page(createEmbed("default", ctx, { + image: { + url: STATIC_ASSETS.card_skeleton + } + })) + + if(this.interactive_components[ctx.data.customId].renderLoadingState) processingEmbed = page(this.interactive_components[ctx.data.customId].renderLoadingState(this)); + await ctx.respond({ + type: InteractionCallbackTypes.UPDATE_MESSAGE, + data: Object.assign(processingEmbed, { components: this._renderComponents(true)}) + }) + + this.activeCardStack = await this.interactive_components[ctx.data.customId].resolvePage(this); + + if(!this.interactive_components[ctx.data.customId].disableCache){ + this._setCachedValue(this.rootIndex, ctx.data.customId, STACK_CACHE_KEYS.RESULT_CARDS, [...this.activeCardStack]); + } + } } catch(e){ this.activeCardStack = [ page(createEmbed("error", ctx, "Stack rendering failed.")) @@ -333,11 +401,9 @@ class DynamicCardStack { console.log("resolve failed:") console.log(e) } - // TODO: allow overriding index - this.index = 0; setTimeout(()=>{ - return this._edit(Object.assign(this.currentPage(), {components:this.listener})) + return this._edit(Object.assign(this.currentPage(), {components:this._renderComponents()})) }, 1500) return; diff --git a/labscore/constants.js b/labscore/constants.js index 143de18..7e0245f 100644 --- a/labscore/constants.js +++ b/labscore/constants.js @@ -1,4 +1,5 @@ const { Permissions } = require("detritus-client/lib/constants") +const { decimalToHexColor } = require("#utils/color"); module.exports.DISCORD_INVITES = Object.freeze({ support: "https://discord.gg/8c4p6xcjru", @@ -40,6 +41,16 @@ module.exports.COLORS = Object.freeze({ incognito: 10057726, }) +module.exports.COLORS_HEX = Object.freeze({ + error: decimalToHexColor(module.exports.COLORS.error), + success: decimalToHexColor(module.exports.COLORS.success), + warning: decimalToHexColor(module.exports.COLORS.warning), + embed: decimalToHexColor(module.exports.COLORS.embed), + brand: decimalToHexColor(module.exports.COLORS.brand), + nsfw: decimalToHexColor(module.exports.COLORS.nsfw), + incognito: decimalToHexColor(module.exports.COLORS.incognito), +}) + // Permission requirements that apply to commands module.exports.PERMISSION_GROUPS = Object.freeze({ // Baseline permission set for regular commands diff --git a/labscore/utils/color.js b/labscore/utils/color.js new file mode 100644 index 0000000..ed77871 --- /dev/null +++ b/labscore/utils/color.js @@ -0,0 +1,7 @@ +module.exports.hexToDecimalColor = (color)=>{ + return parseInt(color.split("#")[1], 16) +} + +module.exports.decimalToHexColor = (color)=>{ + return "#" + color.toString(16); +} \ No newline at end of file diff --git a/labscore/utils/embed.js b/labscore/utils/embed.js index e5423d5..76c1148 100644 --- a/labscore/utils/embed.js +++ b/labscore/utils/embed.js @@ -185,8 +185,4 @@ module.exports.page = function(embed, message = {}, metadata = {}){ embeds: [embed], _meta: metadata, }) -} - -module.exports.hexToEmbedColor = (color)=>{ - return parseInt(color.split("#")[1], 16) } \ No newline at end of file diff --git a/labscore/utils/statics.js b/labscore/utils/statics.js index 8a976e7..f4a8a63 100644 --- a/labscore/utils/statics.js +++ b/labscore/utils/statics.js @@ -10,6 +10,10 @@ const Statics = Object.freeze({ } }, assets: { + card_skeleton: { + file: "loading/04_chat_loading.1zn1ocfb72tc.gif", + revision: 0 + }, chat_loading: { file: "loading/05_chat_loading.7y2ji893rho0.gif", revision: 0 @@ -291,6 +295,7 @@ module.exports.STATIC_ICONS = Object.freeze({ }) module.exports.STATIC_ASSETS = Object.freeze({ + card_skeleton: staticAsset(Statics.assets.card_skeleton), chat_loading: staticAsset(Statics.assets.chat_loading), chat_loading_small: staticAsset(Statics.assets.chat_loading_small), image_loading: staticAsset(Statics.assets.image_loading),