[nextgen/cardstack] add cardstack cloning

This commit is contained in:
bignutty 2025-02-22 13:49:30 +01:00
parent 2e523b1dad
commit e2075b8306

View file

@ -6,7 +6,7 @@ const {editOrReply} = require("#utils/message");
const {STATIC_ASSETS} = require("#utils/statics");
const {Context} = require("detritus-client/lib/command");
const {MessageComponentTypes, InteractionCallbackTypes} = require("detritus-client/lib/constants");
const {MessageComponentTypes, InteractionCallbackTypes, MessageFlags} = require("detritus-client/lib/constants");
const {Message} = require("detritus-client/lib/structures");
const {ComponentContext, Components, ComponentActionRow, ComponentButton} = require("detritus-client/lib/utils");
@ -44,9 +44,12 @@ class DynamicCardStack {
* @param {boolean} options.disableStackCache Allows disabling the stack result cache, meaning that every trigger will reevaluate a stack
* @param {boolean} options.pageNumbers Renders Page Numbers in the footer of all embeds in cards.
* @param {Function} options.pageNumberGenerator Function that renders a page number. Default style is `Page <index>/<total>`
* @param {boolean} options.disableCloning Disables cloning a card stack when someone other than the author interacts with it.
* @param {boolean} options.ephemeral Makes the response ephemeral (primarily used by cloning)
*/
constructor(context, options) {
this.context = context;
this.options = options;
this.cards = options.cards || [];
this.buttons = options.buttons || ["previous", "next"]
@ -56,6 +59,8 @@ class DynamicCardStack {
this.expires = options.expires || 5 * 60 * 1000;
this.pageNumbers = options.pageNumbers || true;
this.pageNumberGenerator = options.pageNumberGenerator || ((pg) => `Page ${pg.index + 1}/${pg.activeCardStack.length}`);
this.disableCloning = options.disableCloning || false;
this.ephemeral = options.ephemeral || false;
this.rootIndex = this.index;
@ -137,7 +142,7 @@ class DynamicCardStack {
* Creates a new cardstack in the given channel
* @private
*/
_spawn() {
_spawn(createMessage = true) {
this._createDynamicCardStack(this.context.client);
this.activeCardStack = [...this.cards];
@ -158,10 +163,12 @@ class DynamicCardStack {
//this.spawned = Date.now()
return this._edit({
if (createMessage) return this._edit({
...this.getCurrentCard(),
components: this.listener
});
return this;
}
/**
@ -217,6 +224,9 @@ class DynamicCardStack {
})
}
// TODO: ensure flags don't get overwritten/allow supplying custom flags
if (this.ephemeral) card.flags = MessageFlags.EPHEMERAL;
return card;
} catch (e) {
console.error("Card rendering failed:")
@ -246,7 +256,7 @@ class DynamicCardStack {
* Decreases the index and returns the next card from the stack.
* @returns {Message} Card
*/
previousPage() {
previousCard() {
this.index = this.index - 1;
if (this.index < 0) {
if (this.loopPages) this.index = this.activeCardStack.length - 1;
@ -267,7 +277,7 @@ class DynamicCardStack {
let message = Object.assign({}, cardContent);
if (!components) {
this.listener.components = this._renderComponents();
this._renderComponents();
message.components = this.listener;
} else {
message.components = components;
@ -279,7 +289,9 @@ class DynamicCardStack {
return editOrReply(this.context, {
...message,
reference: true,
allowedMentions: {parse: [], repliedUser: false}
allowedMentions: {parse: [], repliedUser: false},
// TODO: allow supplying flags
flags: this.ephemeral ? MessageFlags.EPHEMERAL : 0
})
} catch (e) {
console.error("Message editing failed:")
@ -378,7 +390,7 @@ class DynamicCardStack {
// We currently support up to 5 "slots" (action rows),
// although the amount you can actually use depends
// on how many components are added to each slot.
let componentSlots = [[],[],[],[],[]];
let componentSlots = [[], [], [], [], []];
// First Row always starts with built-in components
for (const b of this.buttons) {
@ -419,28 +431,30 @@ class DynamicCardStack {
let renderedSlots = [];
// Render slots
for(const components of componentSlots) {
if(components.length === 0) continue;
for (const components of componentSlots) {
if (components.length === 0) continue;
let row = new ComponentActionRow({});
// Slot all components into their respective rows.
while(components.length > 0) {
while (components.length > 0) {
row.addButton(components.shift());
// Create a new row for content to overflow in.
if(row.isFull){
if (row.isFull) {
renderedSlots.push(row)
row = new ComponentActionRow({});
}
}
// Push rendered row to stack if there are components in it.
if(!row.isEmpty) renderedSlots.push(row);
if (!row.isEmpty) renderedSlots.push(row);
}
if(renderedSlots.length > 5) console.warn("Component Overflow - Limiting to 5.")
return renderedSlots.splice(0, 5);
if (renderedSlots.length > 5) console.warn("Component Overflow - Limiting to 5.")
let compListener = this.listener;
compListener.components = renderedSlots.splice(0, 5)
return compListener;
}
/**
@ -500,9 +514,44 @@ class DynamicCardStack {
* @private
*/
async _handleInteraction(ctx) {
if (ctx.user.id !== this.context.user.id) return ctx.respond({
type: InteractionCallbackTypes.DEFERRED_UPDATE_MESSAGE
})
if (ctx.user.id !== this.context.user.id) {
if (this.disableCloning) return ctx.respond({type: InteractionCallbackTypes.DEFERRED_UPDATE_MESSAGE});
/**
* Card Stack Cloning
*
* This clones the card stack in its current state, calls
* the internal spawn function to "respawn" it under a new
* context, then executes the triggered interaction via
* the new "cloned" cardstack.
*
* This is (maybe?) kind of jank, but I can't think of any
* better ways to ensure state and content consistency
* without it affecting the parent cardstack somehow.
*/
// New message that the new cardstack will attach to.
await ctx.respond(InteractionCallbackTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, {flags: MessageFlags.EPHEMERAL});
let newStack = Object.assign(Object.create(Object.getPrototypeOf(this)), this)
// Reassign the context
newStack.context = ctx;
// Ensure all state is properly cloned to the new stack
newStack.index = this.index;
newStack.rootIndex = this.rootIndex;
newStack.currentSelectedSubcategory = this.currentSelectedSubcategory;
newStack.cards = structuredClone(this.cards);
newStack.activeCardStack = structuredClone(this.activeCardStack);
newStack.currentComponentsBatch = structuredClone(this.currentComponentsBatch);
// Respawn and re-run interaction.
await newStack._spawn(false);
await newStack._handleInteraction(ctx);
return;
}
//this.lastInteraction = Date.now();
@ -510,15 +559,9 @@ class DynamicCardStack {
if (Object.values(BuiltInButtonTypes).includes(ctx.data.customId)) {
switch (ctx.data.customId) {
case "next":
return ctx.respond({
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: this.nextCard()
})
return ctx.editOrRespond(this.nextCard())
case "previous":
return ctx.respond({
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: this.previousPage()
})
return ctx.editOrRespond(this.previousCard())
default:
console.error("unknown button??")
}
@ -537,10 +580,7 @@ class DynamicCardStack {
this.index = this.rootIndex;
this.currentSelectedSubcategory = null;
return await ctx.respond({
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: Object.assign(this.getCurrentCard(), {components: this._renderComponents(false)})
})
return await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}));
} else this.currentSelectedSubcategory = cid;
let resolveTime = Date.now();
@ -549,10 +589,7 @@ class DynamicCardStack {
// If we have a cached result, retrieve it
if (this._getCachedValue(this.rootIndex, cid, STACK_CACHE_KEYS.RESULT_CARDS) !== null) {
this.activeCardStack = [...this._getCachedValue(this.rootIndex, cid, STACK_CACHE_KEYS.RESULT_CARDS)];
await ctx.respond({
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: Object.assign(this.getCurrentCard(), {components: this._renderComponents(false)})
})
await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}));
return;
} else {
// Controls if we should display a loading (skeleton) embed while the
@ -574,10 +611,7 @@ class DynamicCardStack {
if (component.renderLoadingState)
processingEmbed = page(component.renderLoadingState(this, component));
await ctx.respond({
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: Object.assign(processingEmbed, {components: this._renderComponents(true)})
})
await ctx.editOrRespond(Object.assign(processingEmbed, {components: this._renderComponents(true)}))
}
// Compute the active cardstack.
@ -661,10 +695,7 @@ class DynamicCardStack {
// Update the card stack with a card from the new stack.
if (component.instantResult) {
await ctx.respond({
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: Object.assign(this.getCurrentCard(), {components: this._renderComponents()})
})
await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}))
} else {
// This timeout exists 1. for cosmetic reasons so people can
// see the skeleton state and 2. in order to avoid a really
@ -675,13 +706,12 @@ class DynamicCardStack {
// it *should* be safe to just edit the message now.
if ((Date.now() - resolveTime) < 2000) {
setTimeout(() => {
return this._edit(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}))
return ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}))
}, 1500)
} else {
await this._edit(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}))
await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}))
}
}
return;
}